diff --git a/.dockerignore b/.dockerignore index 9cb49913f..6ec863bb3 100644 --- a/.dockerignore +++ b/.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 diff --git a/.drone.yml b/.drone.yml index 802ce0459..3e84af743 100644 --- a/.drone.yml +++ b/.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 diff --git a/.gitignore b/.gitignore index 668986e23..f971dec28 100644 --- a/.gitignore +++ b/.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 diff --git a/Dockerfile b/Dockerfile index 0626441cb..8f12d0c46 100644 --- a/Dockerfile +++ b/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"] \ No newline at end of file +ENTRYPOINT ["dotnet", "Squidex.dll"] \ No newline at end of file diff --git a/Dockerfile.build b/Dockerfile.build deleted file mode 100644 index c2002ae5c..000000000 --- a/Dockerfile.build +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 000000000..7afe7ca48 --- /dev/null +++ b/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 diff --git a/NuGet.Config b/backend/NuGet.Config similarity index 100% rename from NuGet.Config rename to backend/NuGet.Config diff --git a/Squidex.ruleset b/backend/Squidex.ruleset similarity index 100% rename from Squidex.ruleset rename to backend/Squidex.ruleset diff --git a/Squidex.sln b/backend/Squidex.sln similarity index 100% rename from Squidex.sln rename to backend/Squidex.sln diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs new file mode 100644 index 000000000..b1ec2c2d0 --- /dev/null +++ b/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 + { + 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 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; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs rename to backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/ClientPool.cs b/backend/extensions/Squidex.Extensions/Actions/ClientPool.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ClientPool.cs rename to backend/extensions/Squidex.Extensions/Actions/ClientPool.cs diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs rename to backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Email/EmailAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs new file mode 100644 index 000000000..f971a49b7 --- /dev/null +++ b/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 + { + 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 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; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/HttpHelper.cs b/backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/HttpHelper.cs rename to backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs diff --git a/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs rename to backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs rename to backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs new file mode 100644 index 000000000..0815f3baa --- /dev/null +++ b/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 + { + 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 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; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs new file mode 100644 index 000000000..ddd91d9eb --- /dev/null +++ b/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 + { + private const string Description = "Send a tweet"; + + private readonly TwitterOptions twitterOptions; + + public TweetActionHandler(RuleEventFormatter formatter, IOptions 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 ExecuteJobAsync(TweetJob job, CancellationToken ct = default) + { + var tokens = Tokens.Create( + twitterOptions.ClientId, + twitterOptions.ClientSecret, + job.AccessToken, + job.AccessSecret); + + var request = new Dictionary + { + ["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; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs rename to backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs rename to backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs new file mode 100644 index 000000000..ccfa6f308 --- /dev/null +++ b/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 + { + 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 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; } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs rename to backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs diff --git a/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs b/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs similarity index 100% rename from extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs rename to backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs diff --git a/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs b/backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs similarity index 100% rename from extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs rename to backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj new file mode 100644 index 000000000..cca01d821 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -0,0 +1,31 @@ + + + netcoreapp3.0 + 8.0 + + + + + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs new file mode 100644 index 000000000..1a1b64193 --- /dev/null +++ b/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); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs new file mode 100644 index 000000000..f9d7c83a0 --- /dev/null +++ b/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 + { + public static readonly AppClients Empty = new AppClients(); + + private AppClients() + { + } + + public AppClients(KeyValuePair[] 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))); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs new file mode 100644 index 000000000..5d0a81cac --- /dev/null +++ b/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 + { + public static readonly AppContributors Empty = new AppContributors(); + + private AppContributors() + { + } + + public AppContributors(KeyValuePair[] 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)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs new file mode 100644 index 000000000..3d8c59d2c --- /dev/null +++ b/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; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs new file mode 100644 index 000000000..967b891e7 --- /dev/null +++ b/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); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs new file mode 100644 index 000000000..e31daa5e7 --- /dev/null +++ b/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 + { + public static readonly AppPatterns Empty = new AppPatterns(); + + private AppPatterns() + { + } + + public AppPatterns(KeyValuePair[] 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))); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs new file mode 100644 index 000000000..c3ef0856e --- /dev/null +++ b/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); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs new file mode 100644 index 000000000..dfca9aa3b --- /dev/null +++ b/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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs new file mode 100644 index 000000000..d1ac0c353 --- /dev/null +++ b/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 Languages { get; set; } + + [JsonProperty] + public Language? Master { get; set; } + + public JsonLanguagesConfig() + { + } + + public JsonLanguagesConfig(LanguagesConfig value) + { + Languages = new Dictionary(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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs new file mode 100644 index 000000000..1a80e62f0 --- /dev/null +++ b/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 LanguageFallbacks + { + get { return languageFallbacks; } + } + + string IFieldPartitionItem.Key + { + get { return language.Iso2Code; } + } + + string IFieldPartitionItem.Name + { + get { return language.EnglishName; } + } + + IEnumerable IFieldPartitionItem.Fallback + { + get { return LanguageFallbacks.Select(x => x.Iso2Code); } + } + + public LanguageConfig(Language language, bool isOptional = false, IEnumerable? 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(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs new file mode 100644 index 000000000..069c640ec --- /dev/null +++ b/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 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 IEnumerable.GetEnumerator() + { + return languages.Values.GetEnumerator(); + } + + public int Count + { + get { return languages.Count; } + } + + private LanguagesConfig(ArrayDictionary 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 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? 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(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; + }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs new file mode 100644 index 000000000..297f23ec4 --- /dev/null +++ b/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 + { + 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)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs new file mode 100644 index 000000000..035a2bd79 --- /dev/null +++ b/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 inner; + + public static readonly IReadOnlyDictionary Defaults = new Dictionary + { + [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()); + + public int CustomCount + { + get { return inner.Count; } + } + + public Role this[string name] + { + get { return inner[name]; } + } + + public IEnumerable Custom + { + get { return inner.Values; } + } + + public IEnumerable All + { + get { return inner.Values.Union(Defaults.Values); } + } + + private Roles(ArrayDictionary roles) + { + inner = roles; + } + + public Roles(IEnumerable> items) + { + inner = new ArrayDictionary(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[] Cleaned(IEnumerable> items) + { + return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs new file mode 100644 index 000000000..67a3dbd54 --- /dev/null +++ b/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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs new file mode 100644 index 000000000..064700a36 --- /dev/null +++ b/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 : Dictionary, IEquatable> where T : notnull + { + public IEnumerable> ValidValues + { + get { return this.Where(x => x.Value != null); } + } + + protected ContentData(IEqualityComparer comparer) + : base(comparer) + { + } + + protected ContentData(int capacity, IEqualityComparer comparer) + : base(capacity, comparer) + { + } + + protected static TResult MergeTo(TResult target, params TResult[] sources) where TResult : ContentData + { + 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 source, TResult target) where TResult : ContentData + { + 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); + } + + public bool Equals(ContentData? other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs new file mode 100644 index 000000000..d00c21552 --- /dev/null +++ b/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, IEquatable + { + 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(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs new file mode 100644 index 000000000..a4c6c4291 --- /dev/null +++ b/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, IEquatable + { + public IdContentData() + : base(EqualityComparer.Default) + { + } + + public IdContentData(int capacity) + : base(capacity, EqualityComparer.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); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs new file mode 100644 index 000000000..2d31c07e6 --- /dev/null +++ b/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 + { + 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(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."); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs new file mode 100644 index 000000000..7f2ced411 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschrnkt) +// 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 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 { Role }; + } + + ReadOnlyCollection? roles = null; + + if (rolesList != null && rolesList.Count > 0) + { + roles = new ReadOnlyCollection(rolesList); + } + + return new WorkflowTransition(Expression, roles); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs new file mode 100644 index 000000000..aea3e31e7 --- /dev/null +++ b/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, IEquatable + { + 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); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs new file mode 100644 index 000000000..0f2f2ac8b --- /dev/null +++ b/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 + { + 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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs new file mode 100644 index 000000000..f580cfc27 --- /dev/null +++ b/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()!; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs new file mode 100644 index 000000000..9bc70ab86 --- /dev/null +++ b/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 EmptySteps = new Dictionary(); + public static readonly IReadOnlyList EmptySchemaIds = new List(); + public static readonly Workflow Default = CreateDefault(); + public static readonly Workflow Empty = new Workflow(default, EmptySteps); + + public IReadOnlyDictionary Steps { get; } = EmptySteps; + + public IReadOnlyList SchemaIds { get; } = EmptySchemaIds; + + public Status Initial { get; } + + public Workflow( + Status initial, + IReadOnlyDictionary? steps, + IReadOnlyList? 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.Archived] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Archived, true), + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Published] = new WorkflowTransition() + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [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]); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs new file mode 100644 index 000000000..5e5d97217 --- /dev/null +++ b/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 EmptyTransitions = new Dictionary(); + + public IReadOnlyDictionary Transitions { get; } + + public string? Color { get; } + + public bool NoUpdate { get; } + + public WorkflowStep(IReadOnlyDictionary? transitions = null, string? color = null, bool noUpdate = false) + { + Transitions = transitions ?? EmptyTransitions; + + Color = color; + + NoUpdate = noUpdate; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs new file mode 100644 index 000000000..c5cbc4581 --- /dev/null +++ b/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? Roles { get; } + + public WorkflowTransition(string? expression = null, ReadOnlyCollection? roles = null) + { + Expression = expression; + + Roles = roles; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs new file mode 100644 index 000000000..dd18859a3 --- /dev/null +++ b/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 + { + public static readonly Workflows Empty = new Workflows(); + + private Workflows() + { + } + + public Workflows(KeyValuePair[] 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml rename to backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml diff --git a/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd rename to backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd diff --git a/src/Squidex.Domain.Apps.Core.Model/Freezable.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Freezable.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs b/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs new file mode 100644 index 000000000..a15d54402 --- /dev/null +++ b/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 IFieldPartitionItem.Fallback + { + get { return Enumerable.Empty(); } + } + + 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 IEnumerable.GetEnumerator() + { + yield return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs new file mode 100644 index 000000000..66826f9a2 --- /dev/null +++ b/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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs new file mode 100644 index 000000000..d37f73609 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// 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 +{ + public delegate IFieldPartitioning PartitionResolver(Partitioning key); + + public sealed class Partitioning : IEquatable + { + public static readonly Partitioning Invariant = new Partitioning("invariant"); + public static readonly Partitioning Language = new Partitioning("language"); + + public string Key { get; } + + public Partitioning(string key) + { + Guard.NotNullOrEmpty(key); + + Key = key; + } + + public override bool Equals(object? obj) + { + return Equals(obj as Partitioning); + } + + public bool Equals(Partitioning? other) + { + return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public override string ToString() + { + return Key; + } + + public static Partitioning FromString(string? value) + { + var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase); + + return isLanguage ? Language : Invariant; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs new file mode 100644 index 000000000..0dc60efd7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public static class PartitioningExtensions + { + private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Partitioning.Language.Key, + Partitioning.Invariant.Key + }; + + public static bool IsValidPartitioning(this string? value) + { + return value == null || AllowedPartitions.Contains(value); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs new file mode 100644 index 000000000..5d3911ab8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class Rule : Cloneable + { + private RuleTrigger trigger; + private RuleAction action; + private string name; + private bool isEnabled = true; + + public string Name + { + get { return name; } + } + + public RuleTrigger Trigger + { + get { return trigger; } + } + + public RuleAction Action + { + get { return action; } + } + + public bool IsEnabled + { + get { return isEnabled; } + } + + public Rule(RuleTrigger trigger, RuleAction action) + { + Guard.NotNull(trigger); + Guard.NotNull(action); + + this.trigger = trigger; + this.trigger.Freeze(); + + this.action = action; + this.action.Freeze(); + } + + [Pure] + public Rule Rename(string name) + { + return Clone(clone => + { + clone.name = name; + }); + } + + [Pure] + public Rule Enable() + { + return Clone(clone => + { + clone.isEnabled = true; + }); + } + + [Pure] + public Rule Disable() + { + return Clone(clone => + { + clone.isEnabled = false; + }); + } + + [Pure] + public Rule Update(RuleTrigger newTrigger) + { + Guard.NotNull(newTrigger); + + if (newTrigger.GetType() != trigger.GetType()) + { + throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); + } + + newTrigger.Freeze(); + + return Clone(clone => + { + clone.trigger = newTrigger; + }); + } + + [Pure] + public Rule Update(RuleAction newAction) + { + Guard.NotNull(newAction); + + if (newAction.GetType() != action.GetType()) + { + throw new ArgumentException("New action has another type.", nameof(newAction)); + } + + newAction.Freeze(); + + return Clone(clone => + { + clone.action = newAction; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs new file mode 100644 index 000000000..0d797b6f4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + public sealed class ContentChangedTriggerSchemaV2 : Freezable + { + public Guid SchemaId { get; set; } + + public string? Condition { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs new file mode 100644 index 000000000..dffa597bb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// 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 Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ArrayField : RootField, IArrayField + { + private FieldCollection fields = FieldCollection.Empty; + + public IReadOnlyList Fields + { + get { return fields.Ordered; } + } + + public IReadOnlyDictionary FieldsById + { + get { return fields.ById; } + } + + public IReadOnlyDictionary FieldsByName + { + get { return fields.ByName; } + } + + public FieldCollection FieldCollection + { + get { return fields; } + } + + public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + : base(id, name, partitioning, properties, settings) + { + } + + public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + : this(id, name, partitioning, properties, settings) + { + Guard.NotNull(fields); + + this.fields = new FieldCollection(fields); + } + + [Pure] + public ArrayField DeleteField(long fieldId) + { + return Updatefields(f => f.Remove(fieldId)); + } + + [Pure] + public ArrayField ReorderFields(List ids) + { + return Updatefields(f => f.Reorder(ids)); + } + + [Pure] + public ArrayField AddField(NestedField field) + { + return Updatefields(f => f.Add(field)); + } + + [Pure] + public ArrayField UpdateField(long fieldId, Func updater) + { + return Updatefields(f => f.Update(fieldId, updater)); + } + + private ArrayField Updatefields(Func, FieldCollection> updater) + { + var newFields = updater(fields); + + if (ReferenceEquals(newFields, fields)) + { + return this; + } + + return Clone(clone => + { + clone.fields = newFields; + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs new file mode 100644 index 000000000..c0feda7df --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ArrayFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IArrayField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Array(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs new file mode 100644 index 000000000..542b72439 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.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.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class AssetsFieldProperties : FieldProperties + { + public bool MustBeImage { get; set; } + + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public int? MinWidth { get; set; } + + public int? MaxWidth { get; set; } + + public int? MinHeight { get; set; } + + public int? MaxHeight { get; set; } + + public int? MinSize { get; set; } + + public int? MaxSize { get; set; } + + public int? AspectWidth { get; set; } + + public int? AspectHeight { get; set; } + + public bool AllowDuplicates { get; set; } + + public bool ResolveImage { get; set; } + + public ReadOnlyCollection? AllowedExtensions { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Assets(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Assets(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs new file mode 100644 index 000000000..d89db1e8c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class BooleanFieldProperties : FieldProperties + { + public bool? DefaultValue { get; set; } + + public bool InlineEditable { get; set; } + + public BooleanFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Boolean(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Boolean(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs new file mode 100644 index 000000000..f3615d121 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class DateTimeFieldProperties : FieldProperties + { + public Instant? MaxValue { get; set; } + + public Instant? MinValue { get; set; } + + public Instant? DefaultValue { get; set; } + + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } + + public DateTimeFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.DateTime(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.DateTime(id, name, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs new file mode 100644 index 000000000..d19ddfef4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs @@ -0,0 +1,171 @@ +// ========================================================================== +// 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; + +#pragma warning disable IDE0044 // Add readonly modifier + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class FieldCollection : Cloneable> where T : IField + { + public static readonly FieldCollection Empty = new FieldCollection(); + + private static readonly Dictionary EmptyById = new Dictionary(); + private static readonly Dictionary EmptyByString = new Dictionary(); + + private T[] fieldsOrdered; + private Dictionary? fieldsById; + private Dictionary? fieldsByName; + + public IReadOnlyList Ordered + { + get { return fieldsOrdered; } + } + + public IReadOnlyDictionary ById + { + get + { + if (fieldsById == null) + { + if (fieldsOrdered.Length == 0) + { + fieldsById = EmptyById; + } + else + { + fieldsById = fieldsOrdered.ToDictionary(x => x.Id); + } + } + + return fieldsById; + } + } + + public IReadOnlyDictionary ByName + { + get + { + if (fieldsByName == null) + { + if (fieldsOrdered.Length == 0) + { + fieldsByName = EmptyByString; + } + else + { + fieldsByName = fieldsOrdered.ToDictionary(x => x.Name); + } + } + + return fieldsByName; + } + } + + private FieldCollection() + { + fieldsOrdered = Array.Empty(); + } + + public FieldCollection(T[] fields) + { + Guard.NotNull(fields); + + fieldsOrdered = fields; + } + + protected override void OnCloned() + { + fieldsById = null; + fieldsByName = null; + } + + [Pure] + public FieldCollection Remove(long fieldId) + { + if (!ById.TryGetValue(fieldId, out _)) + { + return this; + } + + return Clone(clone => + { + clone.fieldsOrdered = fieldsOrdered.Where(x => x.Id != fieldId).ToArray(); + }); + } + + [Pure] + public FieldCollection Reorder(List ids) + { + Guard.NotNull(ids); + + if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x))) + { + throw new ArgumentException("Ids must cover all fields.", nameof(ids)); + } + + return Clone(clone => + { + clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray(); + }); + } + + [Pure] + public FieldCollection Add(T field) + { + Guard.NotNull(field); + + if (ByName.ContainsKey(field.Name)) + { + throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); + } + + if (ById.ContainsKey(field.Id)) + { + throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field)); + } + + return Clone(clone => + { + clone.fieldsOrdered = clone.fieldsOrdered.Union(Enumerable.Repeat(field, 1)).ToArray(); + }); + } + + [Pure] + public FieldCollection Update(long fieldId, Func updater) + { + Guard.NotNull(updater); + + if (!ById.TryGetValue(fieldId, out var field)) + { + return this; + } + + var newField = updater(field); + + if (ReferenceEquals(newField, field)) + { + return this; + } + + if (!(newField is T)) + { + throw new InvalidOperationException($"Field must be of type {typeof(T)}"); + } + + return Clone(clone => + { + clone.fieldsOrdered = clone.fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x).ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs new file mode 100644 index 000000000..a0578f698 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// 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.Schemas +{ + public abstract class FieldProperties : NamedElementPropertiesBase + { + public bool IsRequired { get; set; } + + public bool IsListField { get; set; } + + public bool IsReferenceField { get; set; } + + public string? Placeholder { get; set; } + + public string? EditorUrl { get; set; } + + public ReadOnlyCollection Tags { get; set; } + + public abstract T Accept(IFieldPropertiesVisitor visitor); + + public abstract T Accept(IFieldVisitor visitor, IField field); + + public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null); + + public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs new file mode 100644 index 000000000..9af19f67f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs @@ -0,0 +1,236 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public static class Fields + { + public static RootField Array(long id, string name, Partitioning partitioning, params NestedField[] fields) + { + return new ArrayField(id, name, partitioning, fields); + } + + public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new ArrayField(id, name, partitioning, properties, settings); + } + + public static RootField Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Json(long id, string name, Partitioning partitioning, JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Number(long id, string name, Partitioning partitioning, NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField References(long id, string name, Partitioning partitioning, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField String(long id, string name, Partitioning partitioning, StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField Tags(long id, string name, Partitioning partitioning, TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static RootField UI(long id, string name, Partitioning partitioning, UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + + public static NestedField Assets(long id, string name, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Boolean(long id, string name, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField DateTime(long id, string name, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Geolocation(long id, string name, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Json(long id, string name, JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Number(long id, string name, NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField References(long id, string name, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField String(long id, string name, StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField Tags(long id, string name, TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static NestedField UI(long id, string name, UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } + + public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func? handler = null, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + { + var field = Array(id, name, partitioning, properties, settings); + + if (handler != null) + { + field = handler(field); + } + + return schema.AddField(field); + } + + public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Assets(id, name, partitioning, properties, settings)); + } + + public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Boolean(id, name, partitioning, properties, settings)); + } + + public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(DateTime(id, name, partitioning, properties, settings)); + } + + public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Geolocation(id, name, partitioning, properties, settings)); + } + + public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning, JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Json(id, name, partitioning, properties, settings)); + } + + public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning, NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Number(id, name, partitioning, properties, settings)); + } + + public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(References(id, name, partitioning, properties, settings)); + } + + public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning, StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(String(id, name, partitioning, properties, settings)); + } + + public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning, TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Tags(id, name, partitioning, properties, settings)); + } + + public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning, UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(UI(id, name, partitioning, properties, settings)); + } + + public static ArrayField AddAssets(this ArrayField field, long id, string name, AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Assets(id, name, properties, settings)); + } + + public static ArrayField AddBoolean(this ArrayField field, long id, string name, BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Boolean(id, name, properties, settings)); + } + + public static ArrayField AddDateTime(this ArrayField field, long id, string name, DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(DateTime(id, name, properties, settings)); + } + + public static ArrayField AddGeolocation(this ArrayField field, long id, string name, GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Geolocation(id, name, properties, settings)); + } + + public static ArrayField AddJson(this ArrayField field, long id, string name, JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Json(id, name, properties, settings)); + } + + public static ArrayField AddNumber(this ArrayField field, long id, string name, NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Number(id, name, properties, settings)); + } + + public static ArrayField AddReferences(this ArrayField field, long id, string name, ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(References(id, name, properties, settings)); + } + + public static ArrayField AddString(this ArrayField field, long id, string name, StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(String(id, name, properties, settings)); + } + + public static ArrayField AddTags(this ArrayField field, long id, string name, TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Tags(id, name, properties, settings)); + } + + public static ArrayField AddUI(this ArrayField field, long id, string name, UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(UI(id, name, properties, settings)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs new file mode 100644 index 000000000..c28ce7b29 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class GeolocationFieldProperties : FieldProperties + { + public GeolocationFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Geolocation(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Geolocation(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs new file mode 100644 index 000000000..06839a5d2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using P = Squidex.Domain.Apps.Core.Partitioning; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class JsonFieldModel : IFieldSettings + { + [JsonProperty] + public long Id { get; set; } + + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Partitioning { get; set; } + + [JsonProperty] + public bool IsHidden { get; set; } + + [JsonProperty] + public bool IsLocked { get; set; } + + [JsonProperty] + public bool IsDisabled { get; set; } + + [JsonProperty] + public FieldProperties Properties { get; set; } + + [JsonProperty] + public JsonNestedFieldModel[]? Children { get; set; } + + public RootField ToField() + { + var partitioning = P.FromString(Partitioning); + + if (Properties is ArrayFieldProperties arrayProperties) + { + var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty(); + + return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); + } + else + { + return Properties.CreateRootField(Id, Name, partitioning, this); + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs new file mode 100644 index 000000000..7f747da01 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -0,0 +1,111 @@ +// ========================================================================== +// 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 Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class JsonSchemaModel + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Category { get; set; } + + [JsonProperty] + public bool IsSingleton { get; set; } + + [JsonProperty] + public bool IsPublished { get; set; } + + [JsonProperty] + public SchemaProperties Properties { get; set; } + + [JsonProperty] + public SchemaScripts Scripts { get; set; } + + [JsonProperty] + public JsonFieldModel[] Fields { get; set; } + + [JsonProperty] + public Dictionary PreviewUrls { get; set; } + + public JsonSchemaModel() + { + } + + public JsonSchemaModel(Schema schema) + { + SimpleMapper.Map(schema, this); + + Fields = + schema.Fields.Select(x => + new JsonFieldModel + { + Id = x.Id, + Name = x.Name, + Children = CreateChildren(x), + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Partitioning = x.Partitioning.Key, + Properties = x.RawProperties + }).ToArray(); + + PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value); + } + + private static JsonNestedFieldModel[]? CreateChildren(IField field) + { + if (field is ArrayField arrayField) + { + return arrayField.Fields.Select(x => + new JsonNestedFieldModel + { + Id = x.Id, + Name = x.Name, + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Properties = x.RawProperties + }).ToArray(); + } + + return null; + } + + public Schema ToSchema() + { + var fields = Fields.Map(f => f.ToField()) ?? Array.Empty(); + + var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton); + + if (!string.IsNullOrWhiteSpace(Category)) + { + schema = schema.ChangeCategory(Category); + } + + if (Scripts != null) + { + schema = schema.ConfigureScripts(Scripts); + } + + if (PreviewUrls?.Count > 0) + { + schema = schema.ConfigurePreviewUrls(PreviewUrls); + } + + return schema; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs new file mode 100644 index 000000000..ae850ec31 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class JsonFieldProperties : FieldProperties + { + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Json(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Json(id, name, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs new file mode 100644 index 000000000..3fae17358 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class NamedElementPropertiesBase : Freezable + { + public string? Label { get; set; } + + public string? Hints { get; set; } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs new file mode 100644 index 000000000..6fcef66be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class NestedField : Cloneable, INestedField + { + private readonly long fieldId; + private readonly string fieldName; + private bool isDisabled; + private bool isHidden; + private bool isLocked; + + public long Id + { + get { return fieldId; } + } + + public string Name + { + get { return fieldName; } + } + + public bool IsLocked + { + get { return isLocked; } + } + + public bool IsHidden + { + get { return isHidden; } + } + + public bool IsDisabled + { + get { return isDisabled; } + } + + public abstract FieldProperties RawProperties { get; } + + protected NestedField(long id, string name, IFieldSettings? settings = null) + { + Guard.NotNullOrEmpty(name); + Guard.GreaterThan(id, 0); + + fieldId = id; + fieldName = name; + + if (settings != null) + { + isLocked = settings.IsLocked; + isHidden = settings.IsHidden; + isDisabled = settings.IsDisabled; + } + } + + [Pure] + public NestedField Lock() + { + return Clone(clone => + { + clone.isLocked = true; + }); + } + + [Pure] + public NestedField Hide() + { + return Clone(clone => + { + clone.isHidden = true; + }); + } + + [Pure] + public NestedField Show() + { + return Clone(clone => + { + clone.isHidden = false; + }); + } + + [Pure] + public NestedField Disable() + { + return Clone(clone => + { + clone.isDisabled = true; + }); + } + + [Pure] + public NestedField Enable() + { + return Clone(clone => + { + clone.isDisabled = false; + }); + } + + public abstract T Accept(IFieldVisitor visitor); + + public abstract NestedField Update(FieldProperties newProperties); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs new file mode 100644 index 000000000..61ba3a6a8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.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.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public class NestedField : NestedField, IField where T : FieldProperties, new() + { + private T properties; + + public T Properties + { + get { return properties; } + } + + public override FieldProperties RawProperties + { + get { return properties; } + } + + public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null) + : base(id, name, settings) + { + SetProperties(properties ?? new T()); + } + + [Pure] + public override NestedField Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); + + return Clone>(clone => + { + clone.SetProperties(typedProperties); + }); + } + + private void SetProperties(T newProperties) + { + properties = newProperties; + properties.Freeze(); + } + + private T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties); + + if (!(newProperties is T typedProperties)) + { + throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + } + + return typedProperties; + } + + public override TResult Accept(IFieldVisitor visitor) + { + return properties.Accept(visitor, this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs new file mode 100644 index 000000000..f38cbe0c3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// 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.Schemas +{ + public sealed class NumberFieldProperties : FieldProperties + { + public ReadOnlyCollection? AllowedValues { get; set; } + + public double? MaxValue { get; set; } + + public double? MinValue { get; set; } + + public double? DefaultValue { get; set; } + + public bool IsUnique { get; set; } + + public bool InlineEditable { get; set; } + + public NumberFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Number(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Number(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs new file mode 100644 index 000000000..31131e4dc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ReferencesFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public bool ResolveReference { get; set; } + + public bool AllowDuplicates { get; set; } + + public ReferencesFieldEditor Editor { get; set; } + + public ReadOnlyCollection? SchemaIds { get; set; } + + public Guid SchemaId + { + set + { + if (value != default) + { + SchemaIds = new ReadOnlyCollection(new List { value }); + } + else + { + SchemaIds = null; + } + } + } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.References(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.References(id, name, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs new file mode 100644 index 000000000..af0f94d07 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs @@ -0,0 +1,122 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class RootField : Cloneable, IRootField + { + private readonly long fieldId; + private readonly string fieldName; + private readonly Partitioning partitioning; + private bool isDisabled; + private bool isHidden; + private bool isLocked; + + public long Id + { + get { return fieldId; } + } + + public string Name + { + get { return fieldName; } + } + + public bool IsLocked + { + get { return isLocked; } + } + + public bool IsHidden + { + get { return isHidden; } + } + + public bool IsDisabled + { + get { return isDisabled; } + } + + public Partitioning Partitioning + { + get { return partitioning; } + } + + public abstract FieldProperties RawProperties { get; } + + protected RootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + Guard.NotNullOrEmpty(name); + Guard.GreaterThan(id, 0); + Guard.NotNull(partitioning); + + fieldId = id; + fieldName = name; + + this.partitioning = partitioning; + + if (settings != null) + { + isLocked = settings.IsLocked; + isHidden = settings.IsHidden; + isDisabled = settings.IsDisabled; + } + } + + [Pure] + public RootField Lock() + { + return Clone(clone => + { + clone.isLocked = true; + }); + } + + [Pure] + public RootField Hide() + { + return Clone(clone => + { + clone.isHidden = true; + }); + } + + [Pure] + public RootField Show() + { + return Clone(clone => + { + clone.isHidden = false; + }); + } + + [Pure] + public RootField Disable() + { + return Clone(clone => + { + clone.isDisabled = true; + }); + } + + [Pure] + public RootField Enable() + { + return Clone(clone => + { + clone.isDisabled = false; + }); + } + + public abstract T Accept(IFieldVisitor visitor); + + public abstract RootField Update(FieldProperties newProperties); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs new file mode 100644 index 000000000..fffc1dc0b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.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.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public class RootField : RootField, IField where T : FieldProperties, new() + { + private T properties; + + public T Properties + { + get { return properties; } + } + + public override FieldProperties RawProperties + { + get { return properties; } + } + + public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null) + : base(id, name, partitioning, settings) + { + SetProperties(properties ?? new T()); + } + + [Pure] + public override RootField Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); + + return Clone>(clone => + { + clone.SetProperties(typedProperties); + }); + } + + private void SetProperties(T newProperties) + { + properties = newProperties; + properties.Freeze(); + } + + private T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties); + + if (!(newProperties is T typedProperties)) + { + throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + } + + return typedProperties; + } + + public override TResult Accept(IFieldVisitor visitor) + { + return properties.Accept(visitor, this); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs new file mode 100644 index 000000000..2777eb65d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -0,0 +1,201 @@ +// ========================================================================== +// 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; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class Schema : Cloneable + { + private static readonly Dictionary EmptyPreviewUrls = new Dictionary(); + private readonly string name; + private readonly bool isSingleton; + private string category; + private FieldCollection fields = FieldCollection.Empty; + private IReadOnlyDictionary previewUrls = EmptyPreviewUrls; + private SchemaScripts scripts = new SchemaScripts(); + private SchemaProperties properties; + private bool isPublished; + + public string Name + { + get { return name; } + } + + public string Category + { + get { return category; } + } + + public bool IsPublished + { + get { return isPublished; } + } + + public bool IsSingleton + { + get { return isSingleton; } + } + + public IReadOnlyList Fields + { + get { return fields.Ordered; } + } + + public IReadOnlyDictionary FieldsById + { + get { return fields.ById; } + } + + public IReadOnlyDictionary FieldsByName + { + get { return fields.ByName; } + } + + public IReadOnlyDictionary PreviewUrls + { + get { return previewUrls; } + } + + public FieldCollection FieldCollection + { + get { return fields; } + } + + public SchemaScripts Scripts + { + get { return scripts; } + } + + public SchemaProperties Properties + { + get { return properties; } + } + + public Schema(string name, SchemaProperties? properties = null, bool isSingleton = false) + { + Guard.NotNullOrEmpty(name); + + this.name = name; + + this.properties = properties ?? new SchemaProperties(); + this.properties.Freeze(); + + this.isSingleton = isSingleton; + } + + public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished, bool isSingleton = false) + : this(name, properties, isSingleton) + { + Guard.NotNull(fields); + + this.fields = new FieldCollection(fields); + + this.isPublished = isPublished; + } + + [Pure] + public Schema Update(SchemaProperties newProperties) + { + Guard.NotNull(newProperties); + + return Clone(clone => + { + clone.properties = newProperties; + clone.properties.Freeze(); + }); + } + + [Pure] + public Schema ConfigureScripts(SchemaScripts newScripts) + { + return Clone(clone => + { + clone.scripts = newScripts ?? new SchemaScripts(); + clone.scripts.Freeze(); + }); + } + + [Pure] + public Schema Publish() + { + return Clone(clone => + { + clone.isPublished = true; + }); + } + + [Pure] + public Schema Unpublish() + { + return Clone(clone => + { + clone.isPublished = false; + }); + } + + [Pure] + public Schema ChangeCategory(string newCategory) + { + return Clone(clone => + { + clone.category = newCategory; + }); + } + + [Pure] + public Schema ConfigurePreviewUrls(IReadOnlyDictionary newPreviewUrls) + { + return Clone(clone => + { + clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls; + }); + } + + [Pure] + public Schema DeleteField(long fieldId) + { + return UpdateFields(f => f.Remove(fieldId)); + } + + [Pure] + public Schema ReorderFields(List ids) + { + return UpdateFields(f => f.Reorder(ids)); + } + + [Pure] + public Schema AddField(RootField field) + { + return UpdateFields(f => f.Add(field)); + } + + [Pure] + public Schema UpdateField(long fieldId, Func updater) + { + return UpdateFields(f => f.Update(fieldId, updater)); + } + + private Schema UpdateFields(Func, FieldCollection> updater) + { + var newFields = updater(fields); + + if (ReferenceEquals(newFields, fields)) + { + return this; + } + + return Clone(clone => + { + clone.fields = newFields; + }); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs new file mode 100644 index 000000000..d58770d83 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// 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.Schemas +{ + public sealed class StringFieldProperties : FieldProperties + { + public ReadOnlyCollection? AllowedValues { get; set; } + + public int? MinLength { get; set; } + + public int? MaxLength { get; set; } + + public bool IsUnique { get; set; } + + public bool InlineEditable { get; set; } + + public string? DefaultValue { get; set; } + + public string? Pattern { get; set; } + + public string? PatternMessage { get; set; } + + public StringFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.String(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.String(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs new file mode 100644 index 000000000..ffa0fc1c5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// 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.Schemas +{ + public sealed class TagsFieldProperties : FieldProperties + { + public ReadOnlyCollection? AllowedValues { get; set; } + + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public TagsFieldEditor Editor { get; set; } + + public TagsFieldNormalization Normalization { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Tags(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Tags(id, name, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs new file mode 100644 index 000000000..cd7741e8c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class UIFieldProperties : FieldProperties + { + public UIFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return new NestedField(id, name, this, settings); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, this, settings); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj new file mode 100644 index 000000000..3cad63271 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -0,0 +1,31 @@ + + + netcoreapp3.0 + Squidex.Domain.Apps.Core + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs rename to backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs new file mode 100644 index 000000000..3ff9d90be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -0,0 +1,160 @@ +// ========================================================================== +// 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.Text; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public static class ContentConverter + { + private static readonly Func KeyNameResolver = f => f.Name; + private static readonly Func KeyIdResolver = f => f.Id; + + public static string ToFullText(this ContentData data, int maxTotalLength = 1024 * 1024, int maxFieldLength = 1000, string separator = " ") where T : notnull + { + var stringBuilder = new StringBuilder(); + + foreach (var fieldValue in data.Values) + { + if (fieldValue != null) + { + foreach (var value in fieldValue.Values) + { + AppendText(value, stringBuilder, maxFieldLength, separator, false); + } + } + } + + var result = stringBuilder.ToString(); + + if (result.Length > maxTotalLength) + { + result = result.Substring(0, maxTotalLength); + } + + return result; + } + + private static void AppendText(IJsonValue value, StringBuilder stringBuilder, int maxFieldLength, string separator, bool allowObjects) + { + if (value.Type == JsonValueType.String) + { + var text = value.ToString(); + + if (text.Length <= maxFieldLength) + { + if (stringBuilder.Length > 0) + { + stringBuilder.Append(separator); + } + + stringBuilder.Append(text); + } + } + else if (value is JsonArray array) + { + foreach (var item in array) + { + AppendText(item, stringBuilder, maxFieldLength, separator, true); + } + } + else if (value is JsonObject obj && allowObjects) + { + foreach (var item in obj.Values) + { + AppendText(item, stringBuilder, maxFieldLength, separator, true); + } + } + } + + public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new NamedContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsById, KeyNameResolver, converters); + } + + public static IdContentData ConvertId2Id(this IdContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new IdContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters); + } + + public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new NamedContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsByName, KeyNameResolver, converters); + } + + public static IdContentData ConvertName2Id(this NamedContentData content, Schema schema, params FieldConverter[] converters) + { + Guard.NotNull(schema); + + var result = new IdContentData(content.Count); + + return ConvertInternal(content, result, schema.FieldsByName, KeyIdResolver, converters); + } + + private static TDict2 ConvertInternal( + TDict1 source, + TDict2 target, + IReadOnlyDictionary fields, + Func targetKey, params FieldConverter[] converters) + where TDict1 : IDictionary + where TDict2 : IDictionary + where TKey1 : notnull + where TKey2 : notnull + { + foreach (var fieldKvp in source) + { + if (!fields.TryGetValue(fieldKvp.Key, out var field)) + { + continue; + } + + ContentFieldData? newValue = fieldKvp.Value; + + if (newValue != null) + { + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, field); + + if (newValue == null) + { + break; + } + } + } + } + + if (newValue != null) + { + target.Add(targetKey(field), newValue); + } + } + + return target; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs new file mode 100644 index 000000000..d313a4d5c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public static class ContentConverterFlat + { + public static object ToFlatLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection? languagePreferences = null) + { + Guard.NotNull(languagesConfig); + + if (languagePreferences == null || languagePreferences.Count == 0) + { + return content; + } + + if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) + { + languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); + } + + var result = new Dictionary(); + + foreach (var fieldValue in content) + { + var fieldData = fieldValue.Value; + + if (fieldData != null) + { + foreach (var language in languagePreferences) + { + if (fieldData.TryGetValue(language, out var value) && value.Type != JsonValueType.Null) + { + result[fieldValue.Key] = value; + + break; + } + } + } + } + + return result; + } + + public static Dictionary ToFlatten(this NamedContentData content) + { + var result = new Dictionary(); + + foreach (var fieldValue in content) + { + var fieldData = fieldValue.Value; + + if (fieldData?.Count == 1) + { + result[fieldValue.Key] = fieldData.Values.First(); + } + else + { + result[fieldValue.Key] = fieldData; + } + } + + return result; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs new file mode 100644 index 000000000..b6d7bc1c8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -0,0 +1,373 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +#pragma warning disable RECS0002 // Convert anonymous method to method group + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public delegate ContentFieldData? FieldConverter(ContentFieldData data, IRootField field); + + public static class FieldConverters + { + private static readonly Func KeyNameResolver = f => f.Name; + private static readonly Func KeyIdResolver = f => f.Id.ToString(); + + private static readonly Func FieldByIdResolver = + (f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null; + + private static readonly Func FieldByNameResolver = + (f, k) => f.FieldsByName.GetOrDefault(k); + + public static FieldConverter ExcludeHidden() + { + return (data, field) => !field.IsForApi() ? null : data; + } + + public static FieldConverter ExcludeChangedTypes() + { + return (data, field) => + { + foreach (var value in data.Values) + { + if (value.Type == JsonValueType.Null) + { + continue; + } + + try + { + JsonValueConverter.ConvertValue(field, value); + } + catch + { + return null; + } + } + + return data; + }; + } + + public static FieldConverter ResolveAssetUrls(IReadOnlyCollection? fields, IAssetUrlGenerator urlGenerator) + { + if (fields?.Any() != true) + { + return (data, field) => data; + } + + var isAll = fields.First() == "*"; + + return (data, field) => + { + if (field is IField && (isAll || fields.Contains(field.Name))) + { + foreach (var partition in data) + { + if (partition.Value is JsonArray array) + { + for (var i = 0; i < array.Count; i++) + { + var id = array[i].ToString(); + + array[i] = JsonValue.Create(urlGenerator.GenerateUrl(id)); + } + } + } + } + + return data; + }; + } + + public static FieldConverter ResolveInvariant(LanguagesConfig config) + { + var codeForInvariant = InvariantPartitioning.Key; + var codeForMasterLanguage = config.Master.Language.Iso2Code; + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Invariant)) + { + var result = new ContentFieldData(); + + if (data.TryGetValue(codeForInvariant, out var value)) + { + result[codeForInvariant] = value; + } + else if (data.TryGetValue(codeForMasterLanguage, out value)) + { + result[codeForInvariant] = value; + } + else if (data.Count > 0) + { + result[codeForInvariant] = data.Values.First(); + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ResolveLanguages(LanguagesConfig config) + { + var codeForInvariant = InvariantPartitioning.Key; + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Language)) + { + var result = new ContentFieldData(); + + foreach (var languageConfig in config) + { + var languageCode = languageConfig.Key; + + if (data.TryGetValue(languageCode, out var value)) + { + result[languageCode] = value; + } + else if (languageConfig == config.Master && data.TryGetValue(codeForInvariant, out value)) + { + result[languageCode] = value; + } + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ResolveFallbackLanguages(LanguagesConfig config) + { + var master = config.Master; + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Language)) + { + foreach (var languageConfig in config) + { + var languageCode = languageConfig.Key; + + if (!data.TryGetValue(languageCode, out var value)) + { + var dataFound = false; + + foreach (var fallback in languageConfig.Fallback) + { + if (data.TryGetValue(fallback, out value)) + { + data[languageCode] = value; + dataFound = true; + break; + } + } + + if (!dataFound && languageConfig != master) + { + if (data.TryGetValue(master.Language, out value)) + { + data[languageCode] = value; + } + } + } + } + } + + return data; + }; + } + + public static FieldConverter FilterLanguages(LanguagesConfig config, IEnumerable? languages) + { + if (languages?.Any() != true) + { + return (data, field) => data; + } + + var languageSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var language in languages) + { + if (config.Contains(language.Iso2Code)) + { + languageSet.Add(language.Iso2Code); + } + } + + if (languageSet.Count == 0) + { + languageSet.Add(config.Master.Language.Iso2Code); + } + + return (data, field) => + { + if (field.Partitioning.Equals(Partitioning.Language)) + { + var result = new ContentFieldData(); + + foreach (var languageCode in languageSet) + { + if (data.TryGetValue(languageCode, out var value)) + { + result[languageCode] = value; + } + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ForNestedName2Name(params ValueConverter[] converters) + { + return ForNested(FieldByNameResolver, KeyNameResolver, converters); + } + + public static FieldConverter ForNestedName2Id(params ValueConverter[] converters) + { + return ForNested(FieldByNameResolver, KeyIdResolver, converters); + } + + public static FieldConverter ForNestedId2Name(params ValueConverter[] converters) + { + return ForNested(FieldByIdResolver, KeyNameResolver, converters); + } + + public static FieldConverter ForNestedId2Id(params ValueConverter[] converters) + { + return ForNested(FieldByIdResolver, KeyIdResolver, converters); + } + + private static FieldConverter ForNested( + Func fieldResolver, + Func keyResolver, + params ValueConverter[] converters) + { + return (data, field) => + { + if (field is IArrayField arrayField) + { + var result = new ContentFieldData(); + + foreach (var partition in data) + { + if (!(partition.Value is JsonArray array)) + { + continue; + } + + var newArray = JsonValue.Array(); + + foreach (var item in array.OfType()) + { + var newItem = JsonValue.Object(); + + foreach (var kvp in item) + { + var nestedField = fieldResolver(arrayField, kvp.Key); + + if (nestedField == null) + { + continue; + } + + var newValue = kvp.Value; + + var isUnset = false; + + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, nestedField); + + if (ReferenceEquals(newValue, Value.Unset)) + { + isUnset = true; + break; + } + } + } + + if (!isUnset) + { + newItem.Add(keyResolver(nestedField), newValue); + } + } + + newArray.Add(newItem); + } + + result.Add(partition.Key, newArray); + } + + return result; + } + + return data; + }; + } + + public static FieldConverter ForValues(params ValueConverter[] converters) + { + return (data, field) => + { + if (!(field is IArrayField)) + { + var result = new ContentFieldData(); + + foreach (var partition in data) + { + var newValue = partition.Value; + + var isUnset = false; + + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, field); + + if (ReferenceEquals(newValue, Value.Unset)) + { + isUnset = true; + break; + } + } + } + + if (!isUnset) + { + result.Add(partition.Key, newValue); + } + } + + return result; + } + + return data; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs new file mode 100644 index 000000000..53d4f7472 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public sealed class ContentEnricher + { + private readonly Schema schema; + private readonly PartitionResolver partitionResolver; + + public ContentEnricher(Schema schema, PartitionResolver partitionResolver) + { + Guard.NotNull(schema); + Guard.NotNull(partitionResolver); + + this.schema = schema; + + this.partitionResolver = partitionResolver; + } + + public void Enrich(NamedContentData data) + { + Guard.NotNull(data); + + foreach (var field in schema.Fields) + { + var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); + + if (fieldData != null) + { + var fieldPartition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in fieldPartition) + { + Enrich(field, fieldData, partitionItem); + } + + if (fieldData.Count > 0) + { + data[field.Name] = fieldData; + } + } + } + } + + private static void Enrich(IField field, ContentFieldData fieldData, IFieldPartitionItem partitionItem) + { + Guard.NotNull(fieldData); + + var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); + + if (field.RawProperties.IsRequired || defaultValue == null || defaultValue.Type == JsonValueType.Null) + { + return; + } + + var key = partitionItem.Key; + + if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value)) + { + fieldData.AddJsonValue(key, defaultValue); + } + } + + private static bool ShouldApplyDefaultValue(IField field, IJsonValue value) + { + return value.Type == JsonValueType.Null || (field is IField && value is JsonScalar s && string.IsNullOrEmpty(s.Value)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs new file mode 100644 index 000000000..2f131b904 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Globalization; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public sealed class DefaultValueFactory : IFieldVisitor + { + private readonly Instant now; + + private DefaultValueFactory(Instant now) + { + this.now = now; + } + + public static IJsonValue CreateDefaultValue(IField field, Instant now) + { + Guard.NotNull(field); + + return field.Accept(new DefaultValueFactory(now)); + } + + public IJsonValue Visit(IArrayField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Create(field.Properties.DefaultValue); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Create(field.Properties.DefaultValue); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Create(field.Properties.DefaultValue); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Array(); + } + + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + + public IJsonValue Visit(IField field) + { + if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) + { + return JsonValue.Create(now.ToString()); + } + + if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) + { + return JsonValue.Create($"{now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); + } + + return JsonValue.Create(field.Properties.DefaultValue?.ToString()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs new file mode 100644 index 000000000..fa7b81d77 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -0,0 +1,224 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.EventSynchronization +{ + public static class SchemaSynchronizer + { + public static IEnumerable Synchronize(this Schema source, Schema? target, IJsonSerializer serializer, Func idGenerator, + SchemaSynchronizationOptions? options = null) + { + Guard.NotNull(source); + Guard.NotNull(serializer); + Guard.NotNull(idGenerator); + + if (target == null) + { + yield return new SchemaDeleted(); + } + else + { + options ??= new SchemaSynchronizationOptions(); + + static SchemaEvent E(SchemaEvent @event) + { + return @event; + } + + if (!source.Properties.EqualsJson(target.Properties, serializer)) + { + yield return E(new SchemaUpdated { Properties = target.Properties }); + } + + if (!source.Category.StringEquals(target.Category)) + { + yield return E(new SchemaCategoryChanged { Name = target.Category }); + } + + if (!source.Scripts.EqualsJson(target.Scripts, serializer)) + { + yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); + } + + if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) + { + yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary(x => x.Key, x => x.Value) }); + } + + if (source.IsPublished != target.IsPublished) + { + yield return target.IsPublished ? + E(new SchemaPublished()) : + E(new SchemaUnpublished()); + } + + var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, CanUpdateRoot, null, options); + + foreach (var @event in events) + { + yield return E(@event); + } + } + } + + private static IEnumerable SyncFields( + FieldCollection source, + FieldCollection target, + IJsonSerializer serializer, + Func idGenerator, + Func canUpdate, + NamedId? parentId, SchemaSynchronizationOptions options) where T : class, IField + { + FieldEvent E(FieldEvent @event) + { + @event.ParentFieldId = parentId; + + return @event; + } + + var sourceIds = new List>(source.Ordered.Select(x => x.NamedId())); + var sourceNames = sourceIds.Select(x => x.Name).ToList(); + + if (!options.NoFieldDeletion) + { + foreach (var sourceField in source.Ordered) + { + if (!target.ByName.TryGetValue(sourceField.Name, out _)) + { + var id = sourceField.NamedId(); + + sourceIds.Remove(id); + sourceNames.Remove(id.Name); + + yield return E(new FieldDeleted { FieldId = id }); + } + } + } + + foreach (var targetField in target.Ordered) + { + NamedId? id = null; + + var canCreateField = true; + + if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) + { + canCreateField = false; + + id = sourceField.NamedId(); + + if (canUpdate(sourceField, targetField)) + { + if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer)) + { + yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); + } + } + else if (!sourceField.IsLocked && !options.NoFieldRecreation) + { + canCreateField = true; + + sourceIds.Remove(id); + sourceNames.Remove(id.Name); + + yield return E(new FieldDeleted { FieldId = id }); + } + } + + if (canCreateField) + { + var partitioning = (string?)null; + + if (targetField is IRootField rootField) + { + partitioning = rootField.Partitioning.Key; + } + + id = NamedId.Of(idGenerator(), targetField.Name); + + yield return new FieldAdded + { + Name = targetField.Name, + ParentFieldId = parentId, + Partitioning = partitioning, + Properties = targetField.RawProperties, + FieldId = id + }; + + sourceIds.Add(id); + sourceNames.Add(id.Name); + } + + if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) + { + if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) + { + yield return E(new FieldLocked { FieldId = id }); + } + + if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) + { + yield return targetField.IsHidden ? + E(new FieldHidden { FieldId = id }) : + E(new FieldShown { FieldId = id }); + } + + if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) + { + yield return targetField.IsDisabled ? + E(new FieldDisabled { FieldId = id }) : + E(new FieldEnabled { FieldId = id }); + } + + if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) + { + var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; + + var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, CanUpdate, id, options); + + foreach (var @event in events) + { + yield return @event; + } + } + } + } + + if (sourceNames.Count > 1) + { + var targetNames = target.Ordered.Select(x => x.Name); + + if (sourceNames.Intersect(targetNames).Count() == target.Ordered.Count && !sourceNames.SequenceEqual(targetNames)) + { + var fieldIds = targetNames.Select(x => sourceIds.FirstOrDefault(y => y.Name == x).Id).ToList(); + + yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; + } + } + } + + private static bool CanUpdateRoot(IRootField source, IRootField target) + { + return CanUpdate(source, target) && source.Partitioning == target.Partitioning; + } + + private static bool CanUpdate(IField source, IField target) + { + return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs new file mode 100644 index 000000000..080340380 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// 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 System.Text; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ContentReferencesExtensions + { + public static IEnumerable GetReferencedIds(this IdContentData source, Schema schema, Ids strategy = Ids.All) + { + Guard.NotNull(schema); + + foreach (var field in schema.Fields) + { + var ids = source.GetReferencedIds(field, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + + public static IEnumerable GetReferencedIds(this IdContentData source, IField field, Ids strategy = Ids.All) + { + Guard.NotNull(field); + + if (source.TryGetValue(field.Id, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var ids = field.GetReferencedIds(partitionValue.Value, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + } + + public static IEnumerable GetReferencedIds(this NamedContentData source, Schema schema, Ids strategy = Ids.All) + { + Guard.NotNull(schema); + + return GetReferencedIds(source, schema.Fields, strategy); + } + + public static IEnumerable GetReferencedIds(this NamedContentData source, IEnumerable fields, Ids strategy = Ids.All) + { + Guard.NotNull(fields); + + foreach (var field in fields) + { + var ids = source.GetReferencedIds(field, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + + public static IEnumerable GetReferencedIds(this NamedContentData source, IField field, Ids strategy = Ids.All) + { + Guard.NotNull(field); + + if (source.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var ids = field.GetReferencedIds(partitionValue.Value, strategy); + + foreach (var id in ids) + { + yield return id; + } + } + } + } + + public static JsonObject FormatReferences(this NamedContentData data, Schema schema, LanguagesConfig languages, string separator = ", ") + { + Guard.NotNull(schema); + + var result = JsonValue.Object(); + + foreach (var language in languages) + { + result[language.Key] = JsonValue.Create(data.FormatReferenceFields(schema, language.Key, separator)); + } + + return result; + } + + private static string FormatReferenceFields(this NamedContentData data, Schema schema, string partition, string separator) + { + Guard.NotNull(schema); + + var sb = new StringBuilder(); + + void AddValue(object value) + { + if (sb.Length > 0) + { + sb.Append(separator); + } + + sb.Append(value); + } + + var referenceFields = schema.Fields.Where(x => x.RawProperties.IsReferenceField); + + if (!referenceFields.Any()) + { + referenceFields = schema.Fields.Take(1); + } + + foreach (var referenceField in referenceFields) + { + if (data.TryGetValue(referenceField.Name, out var fieldData) && fieldData != null) + { + if (fieldData.TryGetValue(partition, out var value)) + { + AddValue(value); + } + else if (fieldData.TryGetValue(InvariantPartitioning.Key, out var value2)) + { + AddValue(value2); + } + } + } + + return sb.ToString(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs new file mode 100644 index 000000000..c9bef6381 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// 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.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public sealed class ReferencesCleaner : IFieldVisitor + { + private readonly IJsonValue value; + private readonly ICollection? oldReferences; + + private ReferencesCleaner(IJsonValue value, ICollection? oldReferences) + { + this.value = value; + + this.oldReferences = oldReferences; + } + + public static IJsonValue CleanReferences(IField field, IJsonValue value, ICollection? oldReferences) + { + return field.Accept(new ReferencesCleaner(value, oldReferences)); + } + + public IJsonValue Visit(IField field) + { + return CleanIds(); + } + + public IJsonValue Visit(IField field) + { + if (oldReferences?.Contains(field.Properties.SingleId()) == true) + { + return JsonValue.Array(); + } + + return CleanIds(); + } + + private IJsonValue CleanIds() + { + var ids = value.ToGuidSet(); + + var isRemoved = false; + + if (oldReferences != null) + { + foreach (var oldReference in oldReferences) + { + isRemoved |= ids.Remove(oldReference); + } + } + + return isRemoved ? ids.ToJsonArray() : value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IField field) + { + return value; + } + + public IJsonValue Visit(IArrayField field) + { + return value; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs new file mode 100644 index 000000000..bc13e06d8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ReferencesExtensions + { + public static IEnumerable GetReferencedIds(this IField field, IJsonValue? value, Ids strategy = Ids.All) + { + return ReferencesExtractor.ExtractReferences(field, value, strategy); + } + + public static IJsonValue CleanReferences(this IField field, IJsonValue value, ICollection? oldReferences) + { + if (IsNull(value)) + { + return value; + } + + return ReferencesCleaner.CleanReferences(field, value, oldReferences); + } + + private static bool IsNull(IJsonValue value) + { + return value == null || value.Type == JsonValueType.Null; + } + + public static JsonArray ToJsonArray(this HashSet ids) + { + var result = JsonValue.Array(); + + foreach (var id in ids) + { + result.Add(JsonValue.Create(id.ToString())); + } + + return result; + } + + public static HashSet ToGuidSet(this IJsonValue? value) + { + if (value is JsonArray array) + { + var result = new HashSet(); + + foreach (var id in array) + { + if (id.Type == JsonValueType.String && Guid.TryParse(id.ToString(), out var guid)) + { + result.Add(guid); + } + } + + return result; + } + + return new HashSet(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs new file mode 100644 index 000000000..dc11ff502 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// 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.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public sealed class ReferencesExtractor : IFieldVisitor> + { + private readonly IJsonValue? value; + private readonly Ids strategy; + + private ReferencesExtractor(IJsonValue? value, Ids strategy) + { + this.value = value; + + this.strategy = strategy; + } + + public static IEnumerable ExtractReferences(IField field, IJsonValue? value, Ids strategy) + { + return field.Accept(new ReferencesExtractor(value, strategy)); + } + + public IEnumerable Visit(IArrayField field) + { + var result = new List(); + + if (value is JsonArray array) + { + foreach (var item in array.OfType()) + { + foreach (var nestedField in field.Fields) + { + if (item.TryGetValue(nestedField.Name, out var nestedValue)) + { + result.AddRange(nestedField.Accept(new ReferencesExtractor(nestedValue, strategy))); + } + } + } + } + + return result; + } + + public IEnumerable Visit(IField field) + { + var ids = value.ToGuidSet(); + + return ids; + } + + public IEnumerable Visit(IField field) + { + var ids = value.ToGuidSet(); + + if (strategy == Ids.All && field.Properties.SchemaIds != null) + { + foreach (var schemaId in field.Properties.SchemaIds) + { + ids.Add(schemaId); + } + } + + return ids; + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs new file mode 100644 index 000000000..83c35f680 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateEdmSchema +{ + public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string names); + + public static class EdmSchemaExtensions + { + public static string EscapeEdmField(this string field) + { + return field.Replace("-", "_"); + } + + public static string UnescapeEdmField(this string field) + { + return field.Replace("_", "-"); + } + + public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory) + { + Guard.NotNull(typeFactory); + Guard.NotNull(partitionResolver); + + var (edmType, _) = typeFactory("Data"); + + var visitor = new EdmTypeVisitor(typeFactory); + + foreach (var field in schema.FieldsByName.Values) + { + if (!field.IsForApi(withHidden)) + { + continue; + } + + var fieldEdmType = field.Accept(visitor); + + if (fieldEdmType == null) + { + continue; + } + + var (partitionType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}"); + + if (created) + { + var partition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partition) + { + partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), fieldEdmType); + } + } + + edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); + } + + return edmType; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs new file mode 100644 index 000000000..0fda2ba53 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateEdmSchema +{ + public sealed class EdmTypeVisitor : IFieldVisitor + { + private readonly EdmTypeFactory typeFactory; + + internal EdmTypeVisitor(EdmTypeFactory typeFactory) + { + this.typeFactory = typeFactory; + } + + public IEdmTypeReference? CreateEdmType(IField field) + { + return field.Accept(this); + } + + public IEdmTypeReference? Visit(IArrayField field) + { + var (fieldEdmType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}.Item"); + + if (created) + { + foreach (var nestedField in field.Fields) + { + var nestedEdmType = nestedField.Accept(this); + + if (nestedEdmType != null) + { + fieldEdmType.AddStructuralProperty(nestedField.Name.EscapeEdmField(), nestedEdmType); + } + } + } + + return new EdmComplexTypeReference(fieldEdmType, false); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return null; + } + + public IEdmTypeReference? Visit(IField field) + { + return null; + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.Double, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference? Visit(IField field) + { + return null; + } + + private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, IField field) + { + return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs new file mode 100644 index 000000000..09e5aa07c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public static class Builder + { + public static JsonSchema Object() + { + return new JsonSchema { Type = JsonObjectType.Object }; + } + + public static JsonSchema Guid() + { + return new JsonSchema { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }; + } + + public static JsonSchema String() + { + return new JsonSchema { Type = JsonObjectType.String }; + } + + public static JsonSchemaProperty ArrayProperty(JsonSchema item) + { + return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }; + } + + public static JsonSchemaProperty BooleanProperty() + { + return new JsonSchemaProperty { Type = JsonObjectType.Boolean }; + } + + public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty GuidProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty ObjectProperty(JsonSchema item, string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired }; + } + + public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs new file mode 100644 index 000000000..306c33fc5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public sealed class ContentSchemaBuilder + { + public JsonSchema CreateContentSchema(Schema schema, JsonSchema dataSchema) + { + Guard.NotNull(schema); + Guard.NotNull(dataSchema); + + var schemaName = schema.Properties.Label.WithFallback(schema.Name); + + var contentSchema = new JsonSchema + { + Properties = + { + ["id"] = Builder.GuidProperty($"The id of the {schemaName} content.", true), + ["data"] = Builder.ObjectProperty(dataSchema, $"The data of the {schemaName}.", true), + ["dataDraft"] = Builder.ObjectProperty(dataSchema, $"The draft data of the {schemaName}.", false), + ["version"] = Builder.NumberProperty($"The version of the {schemaName}.", true), + ["created"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been created.", true), + ["createdBy"] = Builder.StringProperty($"The user that has created the {schemaName} content.", true), + ["lastModified"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been modified last.", true), + ["lastModifiedBy"] = Builder.StringProperty($"The user that has updated the {schemaName} content last.", true), + ["status"] = Builder.StringProperty($"The status of the content.", true) + }, + Type = JsonObjectType.Object + }; + + return contentSchema; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs new file mode 100644 index 000000000..8fb749dc6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public static class JsonSchemaExtensions + { + public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, SchemaResolver schemaResolver, bool withHidden = false) + { + Guard.NotNull(schemaResolver); + Guard.NotNull(partitionResolver); + + var schemaName = schema.Name.ToPascalCase(); + + var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver, withHidden); + var jsonSchema = Builder.Object(); + + foreach (var field in schema.Fields.ForApi(withHidden)) + { + var partitionObject = Builder.Object(); + var partitionSet = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partitionSet) + { + var partitionItemProperty = field.Accept(jsonTypeVisitor); + + if (partitionItemProperty != null) + { + partitionItemProperty.Description = partitionItem.Name; + partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional; + + partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); + } + } + + if (partitionObject.Properties.Count > 0) + { + var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); + + jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference)); + } + } + + return jsonSchema; + } + + public static JsonSchemaProperty CreateProperty(IField field, JsonSchema reference) + { + var jsonProperty = Builder.ObjectProperty(reference); + + if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) + { + jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})"; + } + else + { + jsonProperty.Description = field.Name; + } + + jsonProperty.IsRequired = field.RawProperties.IsRequired; + + return jsonProperty; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs new file mode 100644 index 000000000..35ad7fe26 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -0,0 +1,151 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public delegate JsonSchema SchemaResolver(string name, JsonSchema schema); + + public sealed class JsonTypeVisitor : IFieldVisitor + { + private readonly SchemaResolver schemaResolver; + private readonly bool withHiddenFields; + + public JsonTypeVisitor(SchemaResolver schemaResolver, bool withHiddenFields) + { + this.schemaResolver = schemaResolver; + + this.withHiddenFields = withHiddenFields; + } + + public JsonSchemaProperty? Visit(IArrayField field) + { + var item = Builder.Object(); + + foreach (var nestedField in field.Fields.ForApi(withHiddenFields)) + { + var childProperty = nestedField.Accept(this); + + if (childProperty != null) + { + childProperty.Description = nestedField.RawProperties.Hints; + childProperty.IsRequired = nestedField.RawProperties.IsRequired; + + item.Properties.Add(nestedField.Name, childProperty); + } + } + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + var item = schemaResolver("AssetItem", Builder.Guid()); + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + return Builder.BooleanProperty(); + } + + public JsonSchemaProperty? Visit(IField field) + { + return Builder.DateTimeProperty(); + } + + public JsonSchemaProperty? Visit(IField field) + { + var geolocationSchema = Builder.Object(); + + geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty + { + Type = JsonObjectType.Number, + Minimum = -90, + Maximum = 90, + IsRequired = true + }); + + geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty + { + Type = JsonObjectType.Number, + Minimum = -180, + Maximum = 180, + IsRequired = true + }); + + var reference = schemaResolver("GeolocationDto", geolocationSchema); + + return Builder.ObjectProperty(reference); + } + + public JsonSchemaProperty? Visit(IField field) + { + return Builder.StringProperty(); + } + + public JsonSchemaProperty? Visit(IField field) + { + var property = Builder.NumberProperty(); + + if (field.Properties.MinValue.HasValue) + { + property.Minimum = (decimal)field.Properties.MinValue.Value; + } + + if (field.Properties.MaxValue.HasValue) + { + property.Maximum = (decimal)field.Properties.MaxValue.Value; + } + + return property; + } + + public JsonSchemaProperty? Visit(IField field) + { + var item = schemaResolver("ReferenceItem", Builder.Guid()); + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + var property = Builder.StringProperty(); + + property.MinLength = field.Properties.MinLength; + property.MaxLength = field.Properties.MaxLength; + + if (field.Properties.AllowedValues != null) + { + var names = property.EnumerationNames ??= new Collection(); + + foreach (var value in field.Properties.AllowedValues) + { + names.Add(value); + } + } + + return property; + } + + public JsonSchemaProperty? Visit(IField field) + { + var item = schemaResolver("ReferenceItem", Builder.String()); + + return Builder.ArrayProperty(item); + } + + public JsonSchemaProperty? Visit(IField field) + { + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedManualEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs new file mode 100644 index 000000000..8872ef074 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.Serialization; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents +{ + public abstract class EnrichedUserEventBase : EnrichedEvent + { + public RefToken Actor { get; set; } + + [IgnoreDataMember] + public IUser? User { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs new file mode 100644 index 000000000..2f82c9480 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class EventEnricher : IEventEnricher + { + private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); + private readonly IMemoryCache userCache; + private readonly IUserResolver userResolver; + + public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) + { + Guard.NotNull(userCache); + Guard.NotNull(userResolver); + + this.userCache = userCache; + this.userResolver = userResolver; + } + + public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event) + { + enrichedEvent.Timestamp = @event.Headers.Timestamp(); + + if (enrichedEvent is EnrichedUserEventBase userEvent) + { + if (@event.Payload is SquidexEvent squidexEvent) + { + userEvent.Actor = squidexEvent.Actor; + } + + userEvent.User = await FindUserAsync(userEvent.Actor); + } + + enrichedEvent.AppId = @event.Payload.AppId; + } + + private Task FindUserAsync(RefToken actor) + { + var key = $"EventEnrichers_Users_${actor.Identifier}"; + + return userCache.GetOrCreateAsync(key, async x => + { + x.AbsoluteExpirationRelativeToNow = UserCacheDuration; + + IUser? user; + try + { + user = await userResolver.FindByIdOrEmailAsync(actor.Identifier); + } + catch + { + user = null; + } + + if (user == null && actor.IsClient) + { + user = new ClientUser(actor); + } + + return user; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs new file mode 100644 index 000000000..a74126cc6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleTriggerHandler + { + Type TriggerType { get; } + + Task CreateEnrichedEventAsync(Envelope @event); + + bool Trigger(EnrichedEvent @event, RuleTrigger trigger); + + bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs new file mode 100644 index 000000000..d90a41ac4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class Result + { + public Exception? Exception { get; private set; } + + public string? Dump { get; private set; } + + public RuleResult Status { get; private set; } + + public void Enrich(TimeSpan elapsed) + { + var dumpBuilder = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(Dump)) + { + dumpBuilder.AppendLine(Dump); + } + + if (Status == RuleResult.Timeout) + { + dumpBuilder.AppendLine(); + dumpBuilder.AppendLine("Action timed out."); + } + + dumpBuilder.AppendLine(); + dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); + dumpBuilder.AppendLine(); + + Dump = dumpBuilder.ToString(); + } + + public static Result Ignored() + { + return Success("Ignored"); + } + + public static Result Complete() + { + return Success("Completed"); + } + + public static Result Create(string? dump, RuleResult result) + { + return new Result { Dump = dump, Status = result }; + } + + public static Result Success(string? dump) + { + return new Result { Dump = dump, Status = RuleResult.Success }; + } + + public static Result Failed(Exception? ex) + { + return Failed(ex, ex?.Message); + } + + public static Result SuccessOrFailed(Exception? ex, string? dump) + { + if (ex != null) + { + return Failed(ex, dump); + } + else + { + return Success(dump); + } + } + + public static Result Failed(Exception? ex, string? dump) + { + var result = new Result { Exception = ex, Dump = dump ?? ex?.Message }; + + if (ex is OperationCanceledException || ex is TimeoutException) + { + result.Status = RuleResult.Timeout; + } + else + { + result.Status = RuleResult.Failed; + } + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs new file mode 100644 index 000000000..c2b62a052 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.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.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction + { + private readonly RuleEventFormatter formatter; + + Type IRuleActionHandler.ActionType + { + get { return typeof(TAction); } + } + + Type IRuleActionHandler.DataType + { + get { return typeof(TData); } + } + + protected RuleActionHandler(RuleEventFormatter formatter) + { + Guard.NotNull(formatter); + + this.formatter = formatter; + } + + protected virtual string ToJson(T @event) + { + return formatter.ToPayload(@event); + } + + protected virtual string ToEnvelopeJson(EnrichedEvent @event) + { + return formatter.ToEnvelope(@event); + } + + protected string? Format(Uri uri, EnrichedEvent @event) + { + return formatter.Format(uri.ToString(), @event); + } + + protected string? Format(string text, EnrichedEvent @event) + { + return formatter.Format(text, @event); + } + + async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) + { + var (description, data) = await CreateJobAsync(@event, (TAction)action); + + return (description, data!); + } + + async Task IRuleActionHandler.ExecuteJobAsync(object data, CancellationToken ct) + { + var typedData = (TData)data; + + return await ExecuteJobAsync(typedData, ct); + } + + protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) + { + return Task.FromResult(CreateJob(@event, action)); + } + + protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) + { + throw new NotImplementedException(); + } + + protected abstract Task ExecuteJobAsync(TData job, CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs new file mode 100644 index 000000000..956314daa --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleActionProperty + { + public RuleActionPropertyEditor Editor { get; set; } + + public string Name { get; set; } + + public string Display { get; set; } + + public string? Description { get; set; } + + public bool IsFormattable { get; set; } + + public bool IsRequired { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs new file mode 100644 index 000000000..e90411d69 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleActionRegistration + { + public Type ActionType { get; } + + internal RuleActionRegistration(Type actionType) + { + Guard.NotNull(actionType); + + ActionType = actionType; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs new file mode 100644 index 000000000..7e2ee9b19 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -0,0 +1,314 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// =========================================-================================= + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public class RuleEventFormatter + { + private const string Fallback = "null"; + private const string ScriptSuffix = ")"; + private const string ScriptPrefix = "Script("; + private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray(); + private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray(); + private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); + private static readonly Regex ContentDataPlaceholderNew = new Regex(@"^\{CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}\}", RegexOptions.Compiled); + private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>(); + private readonly IJsonSerializer jsonSerializer; + private readonly IRuleUrlGenerator urlGenerator; + private readonly IScriptEngine scriptEngine; + + public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine) + { + Guard.NotNull(jsonSerializer); + Guard.NotNull(scriptEngine); + Guard.NotNull(urlGenerator); + + this.jsonSerializer = jsonSerializer; + this.scriptEngine = scriptEngine; + this.urlGenerator = urlGenerator; + + AddPattern("APP_ID", AppId); + AddPattern("APP_NAME", AppName); + AddPattern("CONTENT_ACTION", ContentAction); + AddPattern("CONTENT_STATUS", ContentStatus); + AddPattern("CONTENT_URL", ContentUrl); + AddPattern("SCHEMA_ID", SchemaId); + AddPattern("SCHEMA_NAME", SchemaName); + AddPattern("TIMESTAMP_DATETIME", TimestampTime); + AddPattern("TIMESTAMP_DATE", TimestampDate); + AddPattern("USER_ID", UserId); + AddPattern("USER_NAME", UserName); + AddPattern("USER_EMAIL", UserEmail); + } + + private void AddPattern(string placeholder, Func generator) + { + patterns.Add((placeholder.ToCharArray(), generator)); + } + + public virtual string ToPayload(T @event) + { + return jsonSerializer.Serialize(@event); + } + + public virtual string ToEnvelope(EnrichedEvent @event) + { + return jsonSerializer.Serialize(new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }); + } + + public string? Format(string text, EnrichedEvent @event) + { + if (string.IsNullOrWhiteSpace(text)) + { + return text; + } + + var trimmed = text.Trim(); + + if (trimmed.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && trimmed.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase)) + { + var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length); + + var customFunctions = new Dictionary> + { + ["contentUrl"] = () => ContentUrl(@event), + ["contentAction"] = () => ContentAction(@event) + }; + + return scriptEngine.Interpolate("event", @event, script, customFunctions); + } + + var current = text.AsSpan(); + + var sb = new StringBuilder(); + + var cp2 = new ReadOnlySpan(ContentPlaceholderStartNew); + var cp1 = new ReadOnlySpan(ContentPlaceholderStartOld); + + for (var i = 0; i < current.Length; i++) + { + var c = current[i]; + + if (c == '$') + { + sb.Append(current.Slice(0, i).ToString()); + + current = current.Slice(i); + + var test = current.Slice(1); + var tested = false; + + for (var j = 0; j < patterns.Count; j++) + { + var (pattern, replacer) = patterns[j]; + + if (test.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) + { + sb.Append(replacer(@event)); + + current = current.Slice(pattern.Length + 1); + i = 0; + + tested = true; + break; + } + } + + if (!tested && (test.StartsWith(cp1, StringComparison.OrdinalIgnoreCase) || test.StartsWith(cp2, StringComparison.OrdinalIgnoreCase))) + { + var currentString = test.ToString(); + + var match = ContentDataPlaceholderOld.Match(currentString); + + if (!match.Success) + { + match = ContentDataPlaceholderNew.Match(currentString); + } + + if (match.Success) + { + if (@event is EnrichedContentEvent contentEvent) + { + sb.Append(CalculateData(contentEvent.Data, match)); + } + else + { + sb.Append(Fallback); + } + + current = current.Slice(match.Length + 1); + i = 0; + } + } + } + } + + sb.Append(current.ToString()); + + return sb.ToString(); + } + + private static string TimestampDate(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); + } + + private static string TimestampTime(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); + } + + private static string AppId(EnrichedEvent @event) + { + return @event.AppId.Id.ToString(); + } + + private static string AppName(EnrichedEvent @event) + { + return @event.AppId.Name; + } + + private static string SchemaId(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Id.ToString(); + } + + return Fallback; + } + + private static string SchemaName(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Name; + } + + return Fallback; + } + + private static string ContentAction(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return contentEvent.Type.ToString(); + } + + return Fallback; + } + + private static string ContentStatus(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return contentEvent.Status.ToString(); + } + + return Fallback; + } + + private string ContentUrl(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return urlGenerator.GenerateContentUIUrl(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); + } + + return Fallback; + } + + private static string UserName(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.DisplayName() ?? Fallback; + } + + return Fallback; + } + + private static string UserId(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Id ?? Fallback; + } + + return Fallback; + } + + private static string UserEmail(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Email ?? Fallback; + } + + return Fallback; + } + + private static string CalculateData(NamedContentData data, Match match) + { + var captures = match.Groups[2].Captures; + + var path = new string[captures.Count]; + + for (var i = 0; i < path.Length; i++) + { + path[i] = captures[i].Value; + } + + if (!data.TryGetValue(path[0], out var field) || field == null) + { + return Fallback; + } + + if (!field.TryGetValue(path[1], out var value)) + { + return Fallback; + } + + for (var j = 2; j < path.Length; j++) + { + if (value is JsonObject obj && obj.TryGetValue(path[j], out value)) + { + continue; + } + + if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count) + { + value = array[idx]; + } + else + { + return Fallback; + } + } + + if (value == null || value.Type == JsonValueType.Null) + { + return Fallback; + } + + return value.ToString() ?? Fallback; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs new file mode 100644 index 000000000..bb828c223 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs @@ -0,0 +1,189 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable RECS0033 // Convert 'if' to '||' expression + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleRegistry : ITypeProvider + { + private const string ActionSuffix = "Action"; + private const string ActionSuffixV2 = "ActionV2"; + private readonly Dictionary actionTypes = new Dictionary(); + + public IReadOnlyDictionary Actions + { + get { return actionTypes; } + } + + public RuleRegistry(IEnumerable? registrations = null) + { + if (registrations != null) + { + foreach (var registration in registrations) + { + Add(registration.ActionType); + } + } + } + + public void Add() where T : RuleAction + { + Add(typeof(T)); + } + + private void Add(Type actionType) + { + var metadata = actionType.GetCustomAttribute(); + + if (metadata == null) + { + return; + } + + var name = GetActionName(actionType); + + var definition = + new RuleActionDefinition + { + Type = actionType, + Title = metadata.Title, + Display = metadata.Display, + Description = metadata.Description, + IconColor = metadata.IconColor, + IconImage = metadata.IconImage, + ReadMore = metadata.ReadMore + }; + + foreach (var property in actionType.GetProperties()) + { + if (property.CanRead && property.CanWrite) + { + var actionProperty = new RuleActionProperty { Name = property.Name.ToCamelCase(), Display = property.Name }; + + var display = property.GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(display?.Name)) + { + actionProperty.Display = display.Name; + } + + if (!string.IsNullOrWhiteSpace(display?.Description)) + { + actionProperty.Description = display.Description; + } + + var type = property.PropertyType; + + if ((GetDataAttribute(property) != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?)) + { + actionProperty.IsRequired = true; + } + + if (property.GetCustomAttribute() != null) + { + actionProperty.IsFormattable = true; + } + + var dataType = GetDataAttribute(property)?.DataType; + + if (type == typeof(bool) || type == typeof(bool?)) + { + actionProperty.Editor = RuleActionPropertyEditor.Checkbox; + } + else if (type == typeof(int) || type == typeof(int?)) + { + actionProperty.Editor = RuleActionPropertyEditor.Number; + } + else if (dataType == DataType.Url) + { + actionProperty.Editor = RuleActionPropertyEditor.Url; + } + else if (dataType == DataType.Password) + { + actionProperty.Editor = RuleActionPropertyEditor.Password; + } + else if (dataType == DataType.EmailAddress) + { + actionProperty.Editor = RuleActionPropertyEditor.Email; + } + else if (dataType == DataType.MultilineText) + { + actionProperty.Editor = RuleActionPropertyEditor.TextArea; + } + else + { + actionProperty.Editor = RuleActionPropertyEditor.Text; + } + + definition.Properties.Add(actionProperty); + } + } + + actionTypes[name] = definition; + } + + private static T? GetDataAttribute(PropertyInfo property) where T : ValidationAttribute + { + var result = property.GetCustomAttribute(); + + result?.IsValid(null); + + return result; + } + + private static bool IsNullable(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + private static string GetActionName(Type type) + { + return type.TypeName(false, ActionSuffix, ActionSuffixV2); + } + + public void Map(TypeNameRegistry typeNameRegistry) + { + foreach (var actionType in actionTypes.Values) + { + typeNameRegistry.Map(actionType.Type, actionType.Type.Name); + } + + var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract); + + var addedTypes = new HashSet(); + + foreach (var type in eventTypes) + { + if (addedTypes.Add(type)) + { + typeNameRegistry.Map(type, type.Name); + } + } + + var triggerTypes = typeof(RuleTrigger).Assembly.GetTypes().Where(x => typeof(RuleTrigger).IsAssignableFrom(x) && !x.IsAbstract); + + foreach (var type in triggerTypes) + { + if (addedTypes.Add(type)) + { + typeNameRegistry.Map(type, type.Name); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs new file mode 100644 index 000000000..cb2f21066 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -0,0 +1,202 @@ +// ========================================================================== +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public class RuleService + { + private readonly Dictionary ruleActionHandlers; + private readonly Dictionary ruleTriggerHandlers; + private readonly TypeNameRegistry typeNameRegistry; + private readonly RuleOptions ruleOptions; + private readonly IEventEnricher eventEnricher; + private readonly IJsonSerializer jsonSerializer; + private readonly IClock clock; + private readonly ISemanticLog log; + + public RuleService( + IOptions ruleOptions, + IEnumerable ruleTriggerHandlers, + IEnumerable ruleActionHandlers, + IEventEnricher eventEnricher, + IJsonSerializer jsonSerializer, + IClock clock, + ISemanticLog log, + TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(jsonSerializer); + Guard.NotNull(ruleOptions); + Guard.NotNull(ruleTriggerHandlers); + Guard.NotNull(ruleActionHandlers); + Guard.NotNull(typeNameRegistry); + Guard.NotNull(eventEnricher); + Guard.NotNull(clock); + Guard.NotNull(log); + + this.typeNameRegistry = typeNameRegistry; + + this.ruleOptions = ruleOptions.Value; + this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); + this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); + this.eventEnricher = eventEnricher; + + this.jsonSerializer = jsonSerializer; + + this.clock = clock; + + this.log = log; + } + + public virtual async Task CreateJobAsync(Rule rule, Guid ruleId, Envelope @event) + { + Guard.NotNull(rule); + Guard.NotNull(@event); + + try + { + if (!rule.IsEnabled) + { + return null; + } + + if (!(@event.Payload is AppEvent)) + { + return null; + } + + var typed = @event.To(); + + var actionType = rule.Action.GetType(); + + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + return null; + } + + if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + { + return null; + } + + var now = clock.GetCurrentInstant(); + + var eventTime = + @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? + @event.Headers.Timestamp() : + now; + + var expires = eventTime.Plus(Constants.ExpirationTime); + + if (eventTime.Plus(Constants.StaleTime) < now) + { + return null; + } + + if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) + { + return null; + } + + var appEventEnvelope = @event.To(); + + var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); + + if (enrichedEvent == null) + { + return null; + } + + await eventEnricher.EnrichAsync(enrichedEvent, typed); + + if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) + { + return null; + } + + var actionName = typeNameRegistry.GetName(actionType); + var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); + + var json = jsonSerializer.Serialize(actionData.Data); + + var job = new RuleJob + { + Id = Guid.NewGuid(), + ActionData = json, + ActionName = actionName, + AppId = enrichedEvent.AppId.Id, + Created = now, + Description = actionData.Description, + EventName = enrichedEvent.Name, + ExecutionPartition = enrichedEvent.Partition, + Expires = expires, + RuleId = ruleId + }; + + return job; + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "createRuleJob") + .WriteProperty("status", "Failed")); + + return null; + } + } + + public virtual async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) + { + var actionWatch = ValueStopwatch.StartNew(); + + Result result; + + try + { + var actionType = typeNameRegistry.GetType(actionName); + var actionHandler = ruleActionHandlers[actionType]; + + var deserialized = jsonSerializer.Deserialize(job, actionHandler.DataType); + + using (var cts = new CancellationTokenSource(GetTimeoutInMs())) + { + result = await actionHandler.ExecuteJobAsync(deserialized, cts.Token).WithCancellation(cts.Token); + } + } + catch (Exception ex) + { + result = Result.Failed(ex); + } + + var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); + + result.Enrich(elapsed); + + return (result, elapsed); + } + + private int GetTimeoutInMs() + { + return ruleOptions.ExecutionTimeoutInSeconds * 1000; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs new file mode 100644 index 000000000..5a533e5c1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +#pragma warning disable IDE0019 // Use pattern matching + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleTriggerHandler : IRuleTriggerHandler + where TTrigger : RuleTrigger + where TEvent : AppEvent + where TEnrichedEvent : EnrichedEvent + { + public Type TriggerType + { + get { return typeof(TTrigger); } + } + + async Task IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) + { + return await CreateEnrichedEventAsync(@event.To()); + } + + bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) + { + if (@event is TEnrichedEvent typed) + { + return Trigger(typed, (TTrigger)trigger); + } + + return false; + } + + bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) + { + if (@event is TEvent typed) + { + return Trigger(typed, (TTrigger)trigger, ruleId); + } + + return false; + } + + protected abstract Task CreateEnrichedEventAsync(Envelope @event); + + protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); + + protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) + { + return true; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs new file mode 100644 index 000000000..7ba4be400 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +#pragma warning disable RECS0133 // Parameter name differs in base declaration + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentDataObject : ObjectInstance + { + private readonly NamedContentData contentData; + private HashSet fieldsToDelete; + private Dictionary fieldProperties; + private bool isChanged; + + public ContentDataObject(Engine engine, NamedContentData contentData) + : base(engine) + { + Extensible = true; + + this.contentData = contentData; + } + + public void MarkChanged() + { + isChanged = true; + } + + public bool TryUpdate(out NamedContentData result) + { + result = contentData; + + if (isChanged) + { + if (fieldsToDelete != null) + { + foreach (var field in fieldsToDelete) + { + contentData.Remove(field); + } + } + + if (fieldProperties != null) + { + foreach (var kvp in fieldProperties) + { + var value = (ContentDataProperty)kvp.Value; + + if (value.ContentField != null && value.ContentField.TryUpdate(out var fieldData)) + { + contentData[kvp.Key] = fieldData; + } + } + } + } + + return isChanged; + } + + public override void RemoveOwnProperty(string propertyName) + { + if (fieldsToDelete == null) + { + fieldsToDelete = new HashSet(); + } + + fieldsToDelete.Add(propertyName); + fieldProperties?.Remove(propertyName); + + MarkChanged(); + } + + public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) + { + EnsurePropertiesInitialized(); + + if (!fieldProperties.ContainsKey(propertyName)) + { + fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; + } + + return true; + } + + public override void Put(string propertyName, JsValue value, bool throwOnError) + { + EnsurePropertiesInitialized(); + + fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c)).Value = value; + } + + public override PropertyDescriptor GetOwnProperty(string propertyName) + { + EnsurePropertiesInitialized(); + + return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false))); + } + + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + return fieldProperties; + } + + private void EnsurePropertiesInitialized() + { + if (fieldProperties == null) + { + fieldProperties = new Dictionary(contentData.Count); + + foreach (var kvp in contentData) + { + fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs new file mode 100644 index 000000000..eeeb96519 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentDataProperty : CustomProperty + { + private readonly ContentDataObject contentData; + private ContentFieldObject? contentField; + private JsValue value; + + protected override JsValue CustomValue + { + get + { + return value; + } + set + { + if (!Equals(this.value, value)) + { + if (value == null || !value.IsObject()) + { + throw new JavaScriptException("You can only assign objects to content data."); + } + + var obj = value.AsObject(); + + contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); + + foreach (var kvp in obj.GetOwnProperties()) + { + contentField.Put(kvp.Key, kvp.Value.Value, true); + } + + this.value = contentField; + } + } + } + + public ContentFieldObject? ContentField + { + get { return contentField; } + } + + public ContentDataProperty(ContentDataObject contentData, ContentFieldObject? contentField = null) + { + this.contentData = contentData; + this.contentField = contentField; + + if (contentField != null) + { + value = contentField; + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs new file mode 100644 index 000000000..79b54ce2f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -0,0 +1,138 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +#pragma warning disable RECS0133 // Parameter name differs in base declaration + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentFieldObject : ObjectInstance + { + private readonly ContentDataObject contentData; + private readonly ContentFieldData? fieldData; + private HashSet valuesToDelete; + private Dictionary valueProperties; + private bool isChanged; + + public ContentFieldData? FieldData + { + get { return fieldData; } + } + + public ContentFieldObject(ContentDataObject contentData, ContentFieldData? fieldData, bool isNew) + : base(contentData.Engine) + { + Extensible = true; + + this.contentData = contentData; + this.fieldData = fieldData; + + if (isNew) + { + MarkChanged(); + } + } + + public void MarkChanged() + { + isChanged = true; + + contentData.MarkChanged(); + } + + public bool TryUpdate(out ContentFieldData? result) + { + result = fieldData; + + if (isChanged && fieldData != null) + { + if (valuesToDelete != null) + { + foreach (var field in valuesToDelete) + { + fieldData.Remove(field); + } + } + + if (valueProperties != null) + { + foreach (var kvp in valueProperties) + { + var value = (ContentFieldProperty)kvp.Value; + + if (value.IsChanged) + { + fieldData[kvp.Key] = value.ContentValue; + } + } + } + } + + return isChanged; + } + + public override void RemoveOwnProperty(string propertyName) + { + if (valuesToDelete == null) + { + valuesToDelete = new HashSet(); + } + + valuesToDelete.Add(propertyName); + valueProperties?.Remove(propertyName); + + MarkChanged(); + } + + public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) + { + EnsurePropertiesInitialized(); + + if (!valueProperties.ContainsKey(propertyName)) + { + valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; + } + + return true; + } + + public override PropertyDescriptor GetOwnProperty(string propertyName) + { + EnsurePropertiesInitialized(); + + return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; + } + + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + return valueProperties; + } + + private void EnsurePropertiesInitialized() + { + if (valueProperties == null) + { + valueProperties = new Dictionary(fieldData?.Count ?? 0); + + if (fieldData != null) + { + foreach (var kvp in fieldData) + { + valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs new file mode 100644 index 000000000..3136806e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Native; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentFieldProperty : CustomProperty + { + private readonly ContentFieldObject contentField; + private IJsonValue? contentValue; + private JsValue? value; + private bool isChanged; + + protected override JsValue? CustomValue + { + get + { + if (value == null) + { + if (contentValue != null) + { + value = JsonMapper.Map(contentValue, contentField.Engine); + } + } + + return value; + } + set + { + if (!Equals(this.value, value)) + { + this.value = value; + + contentValue = null; + contentField.MarkChanged(); + + isChanged = true; + } + } + } + + public IJsonValue ContentValue + { + get { return contentValue ?? (contentValue = JsonMapper.Map(value)); } + } + + public bool IsChanged + { + get { return isChanged; } + } + + public ContentFieldProperty(ContentFieldObject contentField, IJsonValue? contentValue = null) + { + this.contentField = contentField; + this.contentValue = contentValue; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs new file mode 100644 index 000000000..20c15d600 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public static class JsonMapper + { + public static JsValue Map(IJsonValue? value, Engine engine) + { + if (value == null) + { + return JsValue.Null; + } + + switch (value) + { + case JsonNull _: + return JsValue.Null; + case JsonScalar s: + return new JsString(s.Value); + case JsonScalar b: + return new JsBoolean(b.Value); + case JsonScalar b: + return new JsNumber(b.Value); + case JsonObject obj: + return FromObject(obj, engine); + case JsonArray arr: + return FromArray(arr, engine); + } + + throw new ArgumentException("Invalid json type.", nameof(value)); + } + + private static JsValue FromArray(JsonArray arr, Engine engine) + { + var target = new JsValue[arr.Count]; + + for (var i = 0; i < arr.Count; i++) + { + target[i] = Map(arr[i], engine); + } + + return engine.Array.Construct(target); + } + + private static JsValue FromObject(JsonObject obj, Engine engine) + { + var target = new ObjectInstance(engine); + + foreach (var property in obj) + { + target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true); + } + + return target; + } + + public static IJsonValue Map(JsValue? value) + { + if (value == null || value.IsNull() || value.IsUndefined()) + { + return JsonValue.Null; + } + + if (value.IsString()) + { + return JsonValue.Create(value.AsString()); + } + + if (value.IsBoolean()) + { + return JsonValue.Create(value.AsBoolean()); + } + + if (value.IsNumber()) + { + return JsonValue.Create(value.AsNumber()); + } + + if (value.IsDate()) + { + return JsonValue.Create(value.AsDate().ToString()); + } + + if (value.IsRegExp()) + { + return JsonValue.Create(value.AsRegExp().Value?.ToString()); + } + + if (value.IsArray()) + { + var arr = value.AsArray(); + + var result = JsonValue.Array(); + + for (var i = 0; i < arr.GetLength(); i++) + { + result.Add(Map(arr.Get(i.ToString()))); + } + + return result; + } + + if (value.IsObject()) + { + var obj = value.AsObject(); + + var result = JsonValue.Object(); + + foreach (var kvp in obj.GetOwnProperties()) + { + result[kvp.Key] = Map(kvp.Value.Value); + } + + return result; + } + + throw new ArgumentException("Invalid json type.", nameof(value)); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs new file mode 100644 index 000000000..b65a10901 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class DefaultConverter : IObjectConverter + { + public static readonly DefaultConverter Instance = new DefaultConverter(); + + private DefaultConverter() + { + } + + public bool TryConvert(Engine engine, object value, [MaybeNullWhen(false)] out JsValue result) + { + result = null!; + + if (value is Enum) + { + result = value.ToString(); + return true; + } + + switch (value) + { + case IUser user: + result = JintUser.Create(engine, user); + return true; + case ClaimsPrincipal principal: + result = JintUser.Create(engine, principal); + return true; + case Instant instant: + result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); + return true; + case Status status: + result = status.ToString(); + return true; + case NamedContentData content: + result = new ContentDataObject(engine, content); + return true; + } + + return false; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs new file mode 100644 index 000000000..79883257d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// 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.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public interface IScriptEngine + { + void Execute(ScriptContext context, string script); + + NamedContentData ExecuteAndTransform(ScriptContext context, string script); + + NamedContentData Transform(ScriptContext context, string script); + + bool Evaluate(string name, object context, string script); + + string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs new file mode 100644 index 000000000..2510d9287 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -0,0 +1,312 @@ +// ========================================================================== +// 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.Globalization; +using Esprima; +using Jint; +using Jint.Native; +using Jint.Native.Date; +using Jint.Native.Object; +using Jint.Runtime; +using Jint.Runtime.Interop; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class JintScriptEngine : IScriptEngine + { + public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); + + public void Execute(ScriptContext context, string script) + { + Guard.NotNull(context); + + if (!string.IsNullOrWhiteSpace(script)) + { + var engine = CreateScriptEngine(context); + + EnableDisallow(engine); + EnableReject(engine); + + Execute(engine, script); + } + } + + public NamedContentData ExecuteAndTransform(ScriptContext context, string script) + { + Guard.NotNull(context); + + var result = context.Data!; + + if (!string.IsNullOrWhiteSpace(script)) + { + var engine = CreateScriptEngine(context); + + EnableDisallow(engine); + EnableReject(engine); + + engine.SetValue("operation", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + Execute(engine, script); + } + + return result; + } + + public NamedContentData Transform(ScriptContext context, string script) + { + Guard.NotNull(context); + + var result = context.Data!; + + if (!string.IsNullOrWhiteSpace(script)) + { + try + { + var engine = CreateScriptEngine(context); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + engine.Execute(script); + } + catch (Exception) + { + result = context.Data!; + } + } + + return result; + } + + private static void Execute(Engine engine, string script) + { + try + { + engine.Execute(script); + } + catch (ArgumentException ex) + { + throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); + } + catch (JavaScriptException ex) + { + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + } + catch (ParserException ex) + { + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + } + } + + private Engine CreateScriptEngine(ScriptContext context) + { + var engine = CreateScriptEngine(); + + var contextInstance = new ObjectInstance(engine); + + if (context.Data != null) + { + contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); + } + + if (context.DataOld != null) + { + contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); + } + + if (context.User != null) + { + contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); + } + + if (!string.IsNullOrWhiteSpace(context.Operation)) + { + contextInstance.FastAddProperty("operation", context.Operation, false, false, false); + } + + contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); + + if (context.StatusOld != default) + { + contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); + } + + engine.SetValue("ctx", contextInstance); + engine.SetValue("context", contextInstance); + + return engine; + } + + private Engine CreateScriptEngine(IReferenceResolver? resolver = null, Dictionary>? customFormatters = null) + { + var engine = new Engine(options => + { + if (resolver != null) + { + options.SetReferencesResolver(resolver); + } + + options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); + }); + + if (customFormatters != null) + { + foreach (var kvp in customFormatters) + { + engine.SetValue(kvp.Key, Safe(kvp.Value)); + } + } + + engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); + engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); + engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); + + return engine; + } + + private static Func Safe(Func func) + { + return () => + { + try + { + return func(); + } + catch + { + return "null"; + } + }; + } + + private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) + { + try + { + var stringInput = TypeConverter.ToString(arguments.At(0)); + var single = false; + + if (arguments.Length > 1) + { + single = TypeConverter.ToBoolean(arguments.At(1)); + } + + return stringInput.Slugify(null, single); + } + catch + { + return JsValue.Undefined; + } + } + + private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) + { + try + { + var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); + var dateFormat = TypeConverter.ToString(arguments.At(1)); + + return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); + } + catch + { + return JsValue.Undefined; + } + } + + private static void EnableDisallow(Engine engine) + { + engine.SetValue("disallow", new Action(message => + { + var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; + + throw new DomainForbiddenException(exMessage); + })); + } + + private static void EnableReject(Engine engine) + { + engine.SetValue("reject", new Action(message => + { + var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; + + throw new ValidationException("Script rejected the operation.", errors); + })); + } + + public bool Evaluate(string name, object context, string script) + { + try + { + var result = + CreateScriptEngine(NullPropagation.Instance) + .SetValue(name, context) + .Execute(script) + .GetCompletionValue() + .ToObject(); + + return (bool)result; + } + catch + { + return false; + } + } + + public string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null) + { + try + { + var result = + CreateScriptEngine(NullPropagation.Instance, customFormatters) + .SetValue(name, context) + .Execute(script) + .GetCompletionValue() + .ToObject(); + + var converted = result.ToString(); + + return converted == "undefined" ? "null" : converted; + } + catch (Exception ex) + { + return ex.Message; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs new file mode 100644 index 000000000..a0725cbe5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Jint; +using Jint.Runtime.Interop; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public static class JintUser + { + private static readonly char[] ClaimSeparators = { '/', '.', ':' }; + + public static ObjectWrapper Create(Engine engine, IUser user) + { + var clientId = user.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; + + var isClient = !string.IsNullOrWhiteSpace(clientId); + + return CreateUser(engine, user.Id, isClient, user.Email, user.DisplayName(), user.Claims); + } + + public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) + { + var id = principal.OpenIdSubject()!; + + var isClient = string.IsNullOrWhiteSpace(id); + + if (isClient) + { + id = principal.OpenIdClientId()!; + } + + var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value; + + return CreateUser(engine, id, isClient, principal.OpenIdEmail()!, name, principal.Claims); + } + + private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable allClaims) + { + var claims = + allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) + .ToDictionary( + x => x.Key, + x => x.Select(y => y.Value).ToArray()); + + return new ObjectWrapper(engine, new { id, isClient, email, name, claims }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs new file mode 100644 index 000000000..7d1d89193 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class ScriptContext + { + public ClaimsPrincipal User { get; set; } + + public Guid ContentId { get; set; } + + public NamedContentData? Data { get; set; } + + public NamedContentData DataOld { get; set; } + + public Status Status { get; set; } + + public Status StatusOld { get; set; } + + public string Operation { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj new file mode 100644 index 000000000..bdb436812 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -0,0 +1,33 @@ + + + netcoreapp3.0 + Squidex.Domain.Apps.Core + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs new file mode 100644 index 000000000..281893a1e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public interface ITagService + { + Task> GetTagIdsAsync(Guid appId, string group, HashSet names); + + Task> NormalizeTagsAsync(Guid appId, string group, HashSet? names, HashSet? ids); + + Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids); + + Task GetTagsAsync(Guid appId, string group); + + Task GetExportableTagsAsync(Guid appId, string group); + + Task RebuildTagsAsync(Guid appId, string group, TagsExport tags); + + Task ClearAsync(Guid appId, string group); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs new file mode 100644 index 000000000..8184d8e61 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public static class TagNormalizer + { + public static async Task NormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, NamedContentData newData, NamedContentData? oldData) + { + Guard.NotNull(tagService); + Guard.NotNull(schema); + Guard.NotNull(newData); + + var newValues = new HashSet(); + var newArrays = new List(); + + var oldValues = new HashSet(); + var oldArrays = new List(); + + GetValues(schema, newValues, newArrays, newData); + + if (oldData != null) + { + GetValues(schema, oldValues, oldArrays, oldData); + } + + if (newValues.Count > 0) + { + var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), newValues, oldValues); + + foreach (var array in newArrays) + { + for (var i = 0; i < array.Count; i++) + { + if (normalized.TryGetValue(array[i].ToString(), out var result)) + { + array[i] = JsonValue.Create(result); + } + } + } + } + } + + public static async Task DenormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas) + { + Guard.NotNull(tagService); + Guard.NotNull(schema); + + var tagsValues = new HashSet(); + var tagsArrays = new List(); + + GetValues(schema, tagsValues, tagsArrays, datas); + + if (tagsValues.Count > 0) + { + var denormalized = await tagService.DenormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), tagsValues); + + foreach (var array in tagsArrays) + { + for (var i = 0; i < array.Count; i++) + { + if (denormalized.TryGetValue(array[i].ToString(), out var result)) + { + array[i] = JsonValue.Create(result); + } + } + } + } + } + + private static void GetValues(Schema schema, HashSet values, List arrays, params NamedContentData[] datas) + { + foreach (var field in schema.Fields) + { + if (field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema) + { + foreach (var data in datas) + { + if (data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partition in fieldData) + { + ExtractTags(partition.Value, values, arrays); + } + } + } + } + else if (field is IArrayField arrayField) + { + foreach (var nestedField in arrayField.Fields) + { + if (nestedField is IField nestedTags && nestedTags.Properties.Normalization == TagsFieldNormalization.Schema) + { + foreach (var data in datas) + { + if (data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partition in fieldData) + { + if (partition.Value is JsonArray array) + { + foreach (var value in array) + { + if (value is JsonObject nestedObject) + { + if (nestedObject.TryGetValue(nestedField.Name, out var nestedValue)) + { + ExtractTags(nestedValue, values, arrays); + } + } + } + } + } + } + } + } + } + } + } + } + + private static void ExtractTags(IJsonValue value, ISet values, ICollection arrays) + { + if (value is JsonArray array) + { + foreach (var item in array) + { + if (item.Type == JsonValueType.String) + { + values.Add(item.ToString()); + } + } + + arrays.Add(array); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs new file mode 100644 index 000000000..1c1d209c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +#pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class ContentValidator + { + private readonly Schema schema; + private readonly PartitionResolver partitionResolver; + private readonly ValidationContext context; + private readonly ConcurrentBag errors = new ConcurrentBag(); + + public IReadOnlyCollection Errors + { + get { return errors; } + } + + public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) + { + Guard.NotNull(schema); + Guard.NotNull(context); + Guard.NotNull(partitionResolver); + + this.schema = schema; + this.context = context; + this.partitionResolver = partitionResolver; + } + + private void AddError(IEnumerable path, string message) + { + var pathString = path.ToPathString(); + + errors.Add(new ValidationError(message, pathString)); + } + + public Task ValidatePartialAsync(NamedContentData data) + { + Guard.NotNull(data); + + var validator = CreateSchemaValidator(true); + + return validator.ValidateAsync(data, context, AddError); + } + + public Task ValidateAsync(NamedContentData data) + { + Guard.NotNull(data); + + var validator = CreateSchemaValidator(false); + + return validator.ValidateAsync(data, context, AddError); + } + + private IValidator CreateSchemaValidator(bool isPartial) + { + var fieldsValidators = new Dictionary(schema.Fields.Count); + + foreach (var field in schema.Fields) + { + fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); + } + + return new ObjectValidator(fieldsValidators, isPartial, "field"); + } + + private IValidator CreateFieldValidator(IRootField field, bool isPartial) + { + var partitioning = partitionResolver(field.Partitioning); + + var fieldValidator = field.CreateValidator(); + var fieldsValidators = new Dictionary(); + + foreach (var partition in partitioning) + { + fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator); + } + + return new AggregateValidator( + field.CreateBagValidator() + .Union(Enumerable.Repeat( + new ObjectValidator(fieldsValidators, isPartial, TypeName(field)), 1))); + } + + private static string TypeName(IRootField field) + { + var isLanguage = field.Partitioning.Equals(Partitioning.Language); + + return isLanguage ? "language" : "invariant value"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs new file mode 100644 index 000000000..7d8b842c9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class FieldBagValidatorsFactory : IFieldVisitor> + { + private static readonly FieldBagValidatorsFactory Instance = new FieldBagValidatorsFactory(); + + private FieldBagValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(IField field) + { + Guard.NotNull(field); + + return field.Accept(Instance); + } + + public IEnumerable Visit(IArrayField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield return NoValueValidator.Instance; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs new file mode 100644 index 000000000..a5681d300 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs @@ -0,0 +1,191 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class FieldValueValidatorsFactory : IFieldVisitor> + { + private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory(); + + private FieldValueValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(IField field) + { + Guard.NotNull(field); + + return field.Accept(Instance); + } + + public IEnumerable Visit(IArrayField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + var nestedSchema = new Dictionary(field.Fields.Count); + + foreach (var nestedField in field.Fields) + { + nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator()); + } + + yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + if (!field.Properties.AllowDuplicates) + { + yield return new UniqueValuesValidator(); + } + + yield return new AssetsValidator(field.Properties); + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) + { + yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) + { + yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); + } + + if (field.Properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(field.Properties.AllowedValues); + } + + if (field.Properties.IsUnique) + { + yield return new UniqueValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + if (!field.Properties.AllowDuplicates) + { + yield return new UniqueValuesValidator(); + } + + yield return new ReferencesValidator(field.Properties.SchemaIds); + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired) + { + yield return new RequiredStringValidator(true); + } + + if (field.Properties.MinLength.HasValue || field.Properties.MaxLength.HasValue) + { + yield return new StringLengthValidator(field.Properties.MinLength, field.Properties.MaxLength); + } + + if (!string.IsNullOrWhiteSpace(field.Properties.Pattern)) + { + yield return new PatternValidator(field.Properties.Pattern, field.Properties.PatternMessage); + } + + if (field.Properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(field.Properties.AllowedValues); + } + + if (field.Properties.IsUnique) + { + yield return new UniqueValidator(); + } + } + + public IEnumerable Visit(IField field) + { + if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + { + yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + } + + if (field.Properties.AllowedValues != null) + { + yield return new CollectionItemValidator(new AllowedValuesValidator(field.Properties.AllowedValues)); + } + + yield return new CollectionItemValidator(new RequiredStringValidator(true)); + } + + public IEnumerable Visit(IField field) + { + if (field is INestedField) + { + yield return NoValueValidator.Instance; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs new file mode 100644 index 000000000..d1ffd0067 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -0,0 +1,231 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime.Text; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class JsonValueConverter : IFieldVisitor + { + private readonly IJsonValue value; + + private JsonValueConverter(IJsonValue value) + { + this.value = value; + } + + public static object ConvertValue(IField field, IJsonValue json) + { + return field.Accept(new JsonValueConverter(json)); + } + + public object Visit(IArrayField field) + { + return ConvertToObjectList(); + } + + public object Visit(IField field) + { + return ConvertToGuidList(); + } + + public object Visit(IField field) + { + return ConvertToGuidList(); + } + + public object Visit(IField field) + { + return ConvertToStringList(); + } + + public object Visit(IField field) + { + if (value is JsonScalar b) + { + return b.Value; + } + + throw new InvalidCastException("Invalid json type, expected boolean."); + } + + public object Visit(IField field) + { + if (value is JsonScalar b) + { + return b.Value; + } + + throw new InvalidCastException("Invalid json type, expected number."); + } + + public object Visit(IField field) + { + if (value is JsonScalar b) + { + return b.Value; + } + + throw new InvalidCastException("Invalid json type, expected string."); + } + + public object Visit(IField field) + { + return value; + } + + public object Visit(IField field) + { + if (value.Type == JsonValueType.String) + { + var parseResult = InstantPattern.General.Parse(value.ToString()); + + if (!parseResult.Success) + { + throw parseResult.Exception; + } + + return parseResult.Value; + } + + throw new InvalidCastException("Invalid json type, expected string."); + } + + public object Visit(IField field) + { + if (value is JsonObject geolocation) + { + foreach (var propertyName in geolocation.Keys) + { + if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) && + !string.Equals(propertyName, "longitude", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidCastException("Geolocation can only have latitude and longitude property."); + } + } + + if (geolocation.TryGetValue("latitude", out var latValue) && latValue is JsonScalar latNumber) + { + var lat = latNumber.Value; + + if (!lat.IsBetween(-90, 90)) + { + throw new InvalidCastException("Latitude must be between -90 and 90."); + } + } + else + { + throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); + } + + if (geolocation.TryGetValue("longitude", out var lonValue) && lonValue is JsonScalar lonNumber) + { + var lon = lonNumber.Value; + + if (!lon.IsBetween(-180, 180)) + { + throw new InvalidCastException("Longitude must be between -180 and 180."); + } + } + else + { + throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); + } + + return value; + } + + throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); + } + + public object Visit(IField field) + { + return value; + } + + private object ConvertToGuidList() + { + if (value is JsonArray array) + { + var result = new List(); + + foreach (var item in array) + { + if (item is JsonScalar s && Guid.TryParse(s.Value, out var guid)) + { + result.Add(guid); + } + else + { + throw new InvalidCastException("Invalid json type, expected array of guid strings."); + } + } + + return result; + } + + throw new InvalidCastException("Invalid json type, expected array of guid strings."); + } + + private object ConvertToStringList() + { + if (value is JsonArray array) + { + var result = new List(); + + foreach (var item in array) + { + if (item is JsonNull) + { + result.Add(null); + } + else if (item is JsonScalar s) + { + result.Add(s.Value); + } + else + { + throw new InvalidCastException("Invalid json type, expected array of strings."); + } + } + + return result; + } + + throw new InvalidCastException("Invalid json type, expected array of strings."); + } + + private object ConvertToObjectList() + { + if (value is JsonArray array) + { + var result = new List(); + + foreach (var item in array) + { + if (item is JsonObject obj) + { + result.Add(obj); + } + else + { + throw new InvalidCastException("Invalid json type, expected array of objects."); + } + } + + return result; + } + + throw new InvalidCastException("Invalid json type, expected array of objects."); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs similarity index 100% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs new file mode 100644 index 000000000..0962a5e4b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class Undefined + { + public static readonly object Value = new object(); + + public static bool IsUndefined(this object? other) + { + return ReferenceEquals(other, Value); + } + + public static bool IsNullOrUndefined(this object? other) + { + return other == null || other.IsUndefined(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs new file mode 100644 index 000000000..3bddf058d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -0,0 +1,127 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public delegate Task> CheckContents(Guid schemaId, FilterNode filter); + + public delegate Task> CheckContentsByIds(HashSet ids); + + public delegate Task> CheckAssets(IEnumerable ids); + + public sealed class ValidationContext + { + private readonly Guid contentId; + private readonly Guid schemaId; + private readonly CheckContents checkContent; + private readonly CheckContentsByIds checkContentByIds; + private readonly CheckAssets checkAsset; + private readonly ImmutableQueue propertyPath; + + public ImmutableQueue Path + { + get { return propertyPath; } + } + + public Guid ContentId + { + get { return contentId; } + } + + public Guid SchemaId + { + get { return schemaId; } + } + + public bool IsOptional { get; } + + public ValidationContext( + Guid contentId, + Guid schemaId, + CheckContents checkContent, + CheckContentsByIds checkContentsByIds, + CheckAssets checkAsset) + : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) + { + } + + private ValidationContext( + Guid contentId, + Guid schemaId, + CheckContents checkContent, + CheckContentsByIds checkContentByIds, + CheckAssets checkAsset, + ImmutableQueue propertyPath, + bool isOptional) + { + Guard.NotNull(checkAsset); + Guard.NotNull(checkContent); + Guard.NotNull(checkContentByIds); + + this.propertyPath = propertyPath; + + this.checkContent = checkContent; + this.checkContentByIds = checkContentByIds; + this.checkAsset = checkAsset; + this.contentId = contentId; + + this.schemaId = schemaId; + + IsOptional = isOptional; + } + + public ValidationContext Optional(bool isOptional) + { + return isOptional == IsOptional ? this : OptionalCore(isOptional); + } + + private ValidationContext OptionalCore(bool isOptional) + { + return new ValidationContext( + contentId, + schemaId, + checkContent, + checkContentByIds, + checkAsset, + propertyPath, + isOptional); + } + + public ValidationContext Nested(string property) + { + return new ValidationContext( + contentId, schemaId, + checkContent, + checkContentByIds, + checkAsset, + propertyPath.Enqueue(property), + IsOptional); + } + + public Task> GetContentIdsAsync(HashSet ids) + { + return checkContentByIds(ids); + } + + public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) + { + return checkContent(schemaId, filter); + } + + public Task> GetAssetInfosAsync(IEnumerable assetId) + { + return checkAsset(assetId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs new file mode 100644 index 000000000..2f79b2ba6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AggregateValidator : IValidator + { + private readonly IValidator[]? validators; + + public AggregateValidator(IEnumerable? validators) + { + this.validators = validators?.ToArray(); + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (validators?.Length > 0) + { + return Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError))); + } + + return Task.CompletedTask; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs new file mode 100644 index 000000000..680ee979c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AllowedValuesValidator : IValidator + { + private readonly IEnumerable allowedValues; + + public AllowedValuesValidator(params T[] allowedValues) + : this((IEnumerable)allowedValues) + { + } + + public AllowedValuesValidator(IEnumerable allowedValues) + { + Guard.NotNull(allowedValues); + + this.allowedValues = allowedValues; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value != null && value is T typedValue && !allowedValues.Contains(typedValue)) + { + addError(context.Path, "Not an allowed value."); + } + + return TaskHelper.Done; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs new file mode 100644 index 000000000..a991b4e18 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AssetsValidator : IValidator + { + private readonly AssetsFieldProperties properties; + + public AssetsValidator(AssetsFieldProperties properties) + { + this.properties = properties; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is ICollection assetIds && assetIds.Count > 0) + { + var assets = await context.GetAssetInfosAsync(assetIds); + var index = 0; + + foreach (var assetId in assetIds) + { + index++; + + var path = context.Path.Enqueue($"[{index}]"); + + var asset = assets.FirstOrDefault(x => x.AssetId == assetId); + + if (asset == null) + { + addError(path, $"Id '{assetId}' not found."); + continue; + } + + if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) + { + addError(path, $"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'."); + } + + if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) + { + addError(path, $"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'."); + } + + if (properties.AllowedExtensions != null && + properties.AllowedExtensions.Count > 0 && + !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) + { + addError(path, "Invalid file extension."); + } + + if (!asset.IsImage) + { + if (properties.MustBeImage) + { + addError(path, "Not an image."); + } + + continue; + } + + if (asset.PixelWidth.HasValue && + asset.PixelHeight.HasValue) + { + var w = asset.PixelWidth.Value; + var h = asset.PixelHeight.Value; + + var actualRatio = (double)w / h; + + if (properties.MinWidth.HasValue && w < properties.MinWidth) + { + addError(path, $"Width '{w}px' less than minimum of '{properties.MinWidth}px'."); + } + + if (properties.MaxWidth.HasValue && w > properties.MaxWidth) + { + addError(path, $"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'."); + } + + if (properties.MinHeight.HasValue && h < properties.MinHeight) + { + addError(path, $"Height '{h}px' less than minimum of '{properties.MinHeight}px'."); + } + + if (properties.MaxHeight.HasValue && h > properties.MaxHeight) + { + addError(path, $"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'."); + } + + if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) + { + var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; + + if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) + { + addError(path, $"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'."); + } + } + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs new file mode 100644 index 000000000..6fd866f2b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class CollectionItemValidator : IValidator + { + private readonly IValidator[] itemValidators; + + public CollectionItemValidator(params IValidator[] itemValidators) + { + Guard.NotNull(itemValidators); + Guard.NotEmpty(itemValidators); + + this.itemValidators = itemValidators; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is ICollection items && items.Count > 0) + { + var innerTasks = new List(); + var index = 1; + + foreach (var item in items) + { + var innerContext = context.Nested($"[{index}]"); + + foreach (var itemValidator in itemValidators) + { + innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, addError)); + } + + index++; + } + + await Task.WhenAll(innerTasks); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs new file mode 100644 index 000000000..65ddf8311 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class CollectionValidator : IValidator + { + private readonly bool isRequired; + private readonly int? minItems; + private readonly int? maxItems; + + public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) + { + if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); + } + + this.isRequired = isRequired; + this.minItems = minItems; + this.maxItems = maxItems; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (!(value is ICollection items) || items.Count == 0) + { + if (isRequired && !context.IsOptional) + { + addError(context.Path, "Field is required."); + } + + return TaskHelper.Done; + } + + if (minItems.HasValue && maxItems.HasValue) + { + if (minItems == maxItems && minItems != items.Count) + { + addError(context.Path, $"Must have exactly {maxItems} item(s)."); + } + else if (items.Count < minItems || items.Count > maxItems) + { + addError(context.Path, $"Must have between {minItems} and {maxItems} item(s)."); + } + } + else + { + if (minItems.HasValue && items.Count < minItems.Value) + { + addError(context.Path, $"Must have at least {minItems} item(s)."); + } + + if (maxItems.HasValue && items.Count > maxItems.Value) + { + addError(context.Path, $"Must not have more than {maxItems} item(s)."); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs new file mode 100644 index 000000000..7e306bcde --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class FieldValidator : IValidator + { + private readonly IValidator[] validators; + private readonly IField field; + + public FieldValidator(IEnumerable validators, IField field) + { + Guard.NotNull(field); + + this.validators = validators.ToArray(); + + this.field = field; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + try + { + object? typedValue = value; + + if (value is IJsonValue jsonValue) + { + if (jsonValue.Type == JsonValueType.Null) + { + typedValue = null; + } + else + { + typedValue = JsonValueConverter.ConvertValue(field, jsonValue); + } + } + + if (validators?.Length > 0) + { + var tasks = new List(); + + foreach (var validator in validators) + { + tasks.Add(validator.ValidateAsync(typedValue, context, addError)); + } + + await Task.WhenAll(tasks); + } + } + catch + { + addError(context.Path, "Not a valid value."); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs new file mode 100644 index 000000000..fbe2a92f4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public delegate void AddError(IEnumerable path, string message); + + public interface IValidator + { + Task ValidateAsync(object? value, ValidationContext context, AddError addError); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs new file mode 100644 index 000000000..6e0907836 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class NoValueValidator : IValidator + { + public static readonly NoValueValidator Instance = new NoValueValidator(); + + private NoValueValidator() + { + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (!value.IsUndefined()) + { + addError(context.Path, "Value must not be defined."); + } + + return Task.CompletedTask; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs new file mode 100644 index 000000000..e2b8058df --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class ObjectValidator : IValidator + { + private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); + private readonly IDictionary schema; + private readonly bool isPartial; + private readonly string fieldType; + + public ObjectValidator(IDictionary schema, bool isPartial, string fieldType) + { + this.schema = schema; + this.fieldType = fieldType; + this.isPartial = isPartial; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value.IsNullOrUndefined()) + { + value = DefaultValue; + } + + if (value is IReadOnlyDictionary values) + { + foreach (var fieldData in values) + { + var name = fieldData.Key; + + if (!schema.ContainsKey(name)) + { + addError(context.Path.Enqueue(name), $"Not a known {fieldType}."); + } + } + + var tasks = new List(); + + foreach (var field in schema) + { + var name = field.Key; + + var (isOptional, validator) = field.Value; + + object? fieldValue = Undefined.Value; + + if (!values.TryGetValue(name, out var temp)) + { + if (isPartial) + { + continue; + } + } + else + { + fieldValue = temp; + } + + var fieldContext = context.Nested(name).Optional(isOptional); + + tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError)); + } + + await Task.WhenAll(tasks); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs new file mode 100644 index 000000000..97d9cf06d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class PatternValidator : IValidator + { + private static readonly TimeSpan Timeout = TimeSpan.FromMilliseconds(20); + private readonly Regex regex; + private readonly string? errorMessage; + + public PatternValidator(string pattern, string? errorMessage = null) + { + this.errorMessage = errorMessage; + + regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout); + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is string stringValue) + { + if (!string.IsNullOrEmpty(stringValue)) + { + try + { + if (!regex.IsMatch(stringValue)) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + { + addError(context.Path, "Does not match to the pattern."); + } + else + { + addError(context.Path, errorMessage); + } + } + } + catch + { + addError(context.Path, "Regex is too slow."); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs new file mode 100644 index 000000000..a1133f0c5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.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.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class RangeValidator : IValidator where T : struct, IComparable + { + private readonly T? min; + private readonly T? max; + + public RangeValidator(T? min, T? max) + { + if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) > 0) + { + throw new ArgumentException("Min value must be greater than max value.", nameof(min)); + } + + this.min = min; + this.max = max; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value != null && value is T typedValue) + { + if (min.HasValue && max.HasValue) + { + if (Equals(min, max) && Equals(min.Value, max.Value)) + { + addError(context.Path, $"Must be exactly '{max}'."); + } + else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) + { + addError(context.Path, $"Must be between '{min}' and '{max}'."); + } + } + else + { + if (min.HasValue && typedValue.CompareTo(min.Value) < 0) + { + addError(context.Path, $"Must be greater or equal to '{min}'."); + } + + if (max.HasValue && typedValue.CompareTo(max.Value) > 0) + { + addError(context.Path, $"Must be less or equal to '{max}'."); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs new file mode 100644 index 000000000..09815efd4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// 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 System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class ReferencesValidator : IValidator + { + private readonly IEnumerable? schemaIds; + + public ReferencesValidator(IEnumerable? schemaIds) + { + this.schemaIds = schemaIds; + } + + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is ICollection contentIds) + { + var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); + + foreach (var id in contentIds) + { + var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id); + + if (schemaId == Guid.Empty) + { + addError(context.Path, $"Contains invalid reference '{id}'."); + } + else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId)) + { + addError(context.Path, $"Contains reference '{id}' to invalid schema."); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs new file mode 100644 index 000000000..97e411a09 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredStringValidator : IValidator + { + private readonly bool validateEmptyStrings; + + public RequiredStringValidator(bool validateEmptyStrings = false) + { + this.validateEmptyStrings = validateEmptyStrings; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (context.IsOptional) + { + return TaskHelper.Done; + } + + if (value.IsNullOrUndefined() || IsEmptyString(value)) + { + addError(context.Path, "Field is required."); + } + + return TaskHelper.Done; + } + + private bool IsEmptyString(object? value) + { + return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs new file mode 100644 index 000000000..ef9cf1027 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredValidator : IValidator + { + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value.IsNullOrUndefined() && !context.IsOptional) + { + addError(context.Path, "Field is required."); + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs new file mode 100644 index 000000000..08be9d0b0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.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.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class StringLengthValidator : IValidator + { + private readonly int? minLength; + private readonly int? maxLength; + + public StringLengthValidator(int? minLength, int? maxLength) + { + if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); + } + + this.minLength = minLength; + this.maxLength = maxLength; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + if (minLength.HasValue && maxLength.HasValue) + { + if (minLength == maxLength && minLength != stringValue.Length) + { + addError(context.Path, $"Must have exactly {maxLength} character(s)."); + } + else if (stringValue.Length < minLength || stringValue.Length > maxLength) + { + addError(context.Path, $"Must have between {minLength} and {maxLength} character(s)."); + } + } + else + { + if (minLength.HasValue && stringValue.Length < minLength.Value) + { + addError(context.Path, $"Must have at least {minLength} character(s)."); + } + + if (maxLength.HasValue && stringValue.Length > maxLength.Value) + { + addError(context.Path, $"Must not have more than {maxLength} character(s)."); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs new file mode 100644 index 000000000..fee6cf8e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class UniqueValidator : IValidator + { + public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + var count = context.Path.Count(); + + if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) + { + FilterNode? filter = null; + + if (value is string s) + { + filter = ClrFilter.Eq(Path(context), s); + } + else if (value is double d) + { + filter = ClrFilter.Eq(Path(context), d); + } + + if (filter != null) + { + var found = await context.GetContentIdsAsync(context.SchemaId, filter); + + if (found.Any(x => x.Id != context.ContentId)) + { + addError(context.Path, "Another content with the same value exists."); + } + } + } + } + + private static List Path(ValidationContext context) + { + return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs new file mode 100644 index 000000000..c9e9f724f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class UniqueValuesValidator : IValidator + { + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is IEnumerable items && items.Any()) + { + var itemsArray = items.ToArray(); + + if (itemsArray.Length != itemsArray.Distinct().Count()) + { + addError(context.Path, "Must not contain duplicate values."); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs new file mode 100644 index 000000000..536eaee23 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// 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 System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository + { + public MongoAssetRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "States_Assets"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Tags) + .Descending(x => x.LastModified)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Slug)) + }, ct); + } + + public async Task> QueryAsync(Guid appId, ClrQuery query) + { + using (Profiler.TraceMethod("QueryAsyncByQuery")) + { + try + { + query = query.AdjustToModel(); + + var filter = query.BuildFilter(appId); + + var contentCount = Collection.Find(filter).CountDocumentsAsync(); + var contentItems = + Collection.Find(filter) + .AssetTake(query) + .AssetSkip(query) + .AssetSort(query) + .ToListAsync(); + + await Task.WhenAll(contentItems, contentCount); + + return ResultList.Create(contentCount.Result, contentItems.Result); + } + catch (MongoQueryException ex) + { + if (ex.Message.Contains("17406")) + { + throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); + } + else + { + throw; + } + } + } + } + + public async Task> QueryAsync(Guid appId, HashSet ids) + { + using (Profiler.TraceMethod("QueryAsyncByIds")) + { + var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified); + + var assetItems = await find.ToListAsync(); + + return ResultList.Create(assetItems.Count, assetItems.OfType()); + } + } + + public async Task FindAssetBySlugAsync(Guid appId, string slug) + { + using (Profiler.TraceMethod()) + { + var assetEntity = + await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.Slug == slug) + .FirstOrDefaultAsync(); + + return assetEntity; + } + } + + public async Task> QueryByHashAsync(Guid appId, string hash) + { + using (Profiler.TraceMethod()) + { + var assetEntities = + await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash) + .ToListAsync(); + + return assetEntities.OfType().ToList(); + } + } + + public async Task FindAssetAsync(Guid id, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var assetEntity = + await Collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); + + if (assetEntity?.IsDeleted == true && !allowDeleted) + { + return null; + } + + return assetEntity; + } + } + + public Task RemoveAsync(Guid key) + { + return Collection.DeleteOneAsync(x => x.Id == key); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs new file mode 100644 index 000000000..c50f90efd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed partial class MongoAssetRepository : ISnapshotStore + { + async Task<(AssetState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (Map(existing), existing.Version); + } + + return (null!, EtagVersion.NotFound); + } + } + + async Task ISnapshotStore.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod()) + { + var entity = SimpleMapper.Map(value, new MongoAssetEntity()); + + entity.Version = newVersion; + entity.IndexedAppId = value.AppId.Id; + + await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); + } + } + + async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + { + using (Profiler.TraceMethod()) + { + await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct); + } + } + + async Task ISnapshotStore.RemoveAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + await Collection.DeleteOneAsync(x => x.Id == key); + } + } + + private static AssetState Map(MongoAssetEntity existing) + { + return SimpleMapper.Map(existing, new AssetState()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs new file mode 100644 index 000000000..9485857b9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -0,0 +1,271 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + internal class MongoContentCollection : MongoRepositoryBase + { + private readonly IAppProvider appProvider; + private readonly IJsonSerializer serializer; + + public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) + : base(database) + { + this.appProvider = appProvider; + + this.serializer = serializer; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel(Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.ScheduledAt) + .Ascending(x => x.IsDeleted)), + new CreateIndexModel(Index + .Ascending(x => x.ReferencedIds)) + }, ct); + } + + protected override string CollectionName() + { + return "State_Contents"; + } + + public async Task> QueryAsync(ISchemaEntity schema, ClrQuery query, List? ids, Status[]? status, bool inDraft, bool includeDraft = true) + { + try + { + query = query.AdjustToModel(schema.SchemaDef, inDraft); + + var filter = query.ToFilter(schema.Id, ids, status); + + var contentCount = Collection.Find(filter).CountDocumentsAsync(); + var contentItems = + Collection.Find(filter) + .WithoutDraft(includeDraft) + .ContentTake(query) + .ContentSkip(query) + .ContentSort(query) + .ToListAsync(); + + await Task.WhenAll(contentItems, contentCount); + + foreach (var entity in contentItems.Result) + { + entity.ParseData(schema.SchemaDef, serializer); + } + + return ResultList.Create(contentCount.Result, contentItems.Result); + } + catch (MongoQueryException ex) + { + if (ex.Message.Contains("17406")) + { + throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); + } + else + { + throw; + } + } + } + + public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[]? status, bool includeDraft) + { + var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); + + var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); + + var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); + var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); + + var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); + + foreach (var entity in contentItems) + { + var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId); + + if (schema != null) + { + entity.ParseData(schema.SchemaDef, serializer); + + result.Add((entity, schema)); + } + } + + return result; + } + + public async Task> QueryAsync(ISchemaEntity schema, HashSet ids, Status[]? status, bool includeDraft) + { + var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); + + var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); + + foreach (var entity in contentItems) + { + entity.ParseData(schema.SchemaDef, serializer); + } + + return ResultList.Create(contentItems.Count, contentItems); + } + + public async Task FindContentAsync(ISchemaEntity schema, Guid id, Status[]? status, bool includeDraft) + { + var find = Collection.Find(FilterFactory.Build(schema.Id, id, status)); + + var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync(); + + contentEntity?.ParseData(schema.SchemaDef, serializer); + + return contentEntity; + } + + public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) + .Not(x => x.DataByIds) + .Not(x => x.DataDraftByIds) + .ForEachAsync(c => + { + callback(c); + }); + } + + public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) + { + var filter = filterNode.AdjustToModel(schema.SchemaDef, true)?.ToFilter(schema.Id); + + var contentEntities = + await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) + .ToListAsync(); + + return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); + } + + public async Task> QueryIdsAsync(HashSet ids) + { + var contentEntities = + await Collection.Find(Filter.In(x => x.Id, ids)).Only(x => x.Id, x => x.IndexedSchemaId) + .ToListAsync(); + + return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); + } + + public async Task> QueryIdsAsync(Guid appId) + { + var contentEntities = + await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) + .ToListAsync(); + + return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); + } + + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema) + { + var contentEntity = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (contentEntity != null) + { + var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); + + contentEntity.ParseData(schema.SchemaDef, serializer); + + return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); + } + + return (null!, EtagVersion.NotFound); + } + + public Task ReadAllAsync(Func callback, Func> getSchema, CancellationToken ct = default) + { + return Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(async contentEntity => + { + var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); + + contentEntity.ParseData(schema.SchemaDef, serializer); + + await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); + }, ct); + } + + public Task CleanupAsync(Guid id) + { + return Collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, id), + Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), + Update.AddToSet(x => x.ReferencedIdsDeleted, id)); + } + + public Task RemoveAsync(Guid id) + { + return Collection.DeleteOneAsync(x => x.Id == id); + } + + public async Task UpsertAsync(MongoContentEntity content, long oldVersion) + { + try + { + await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs new file mode 100644 index 000000000..96e991253 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public sealed class MongoContentEntity : IContentEntity + { + private NamedContentData? data; + private NamedContentData dataDraft; + + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement("_ai")] + [BsonRepresentation(BsonType.String)] + public Guid IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("_si")] + [BsonRepresentation(BsonType.String)] + public Guid IndexedSchemaId { get; set; } + + [BsonRequired] + [BsonElement("rf")] + [BsonRepresentation(BsonType.String)] + public List? ReferencedIds { get; set; } + + [BsonRequired] + [BsonElement("rd")] + [BsonRepresentation(BsonType.String)] + public List ReferencedIdsDeleted { get; set; } = new List(); + + [BsonRequired] + [BsonElement("ss")] + public Status Status { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("do")] + [BsonJson] + public IdContentData DataByIds { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("dd")] + [BsonJson] + public IdContentData DataDraftByIds { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("sj")] + [BsonJson] + public ScheduleJob? ScheduleJob { get; set; } + + [BsonRequired] + [BsonElement("ai")] + public NamedId AppId { get; set; } + + [BsonRequired] + [BsonElement("si")] + public NamedId SchemaId { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("sa")] + public Instant? ScheduledAt { get; set; } + + [BsonRequired] + [BsonElement("ct")] + public Instant Created { get; set; } + + [BsonRequired] + [BsonElement("mt")] + public Instant LastModified { get; set; } + + [BsonRequired] + [BsonElement("vs")] + public long Version { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("dl")] + public bool IsDeleted { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("pd")] + public bool IsPending { get; set; } + + [BsonRequired] + [BsonElement("cb")] + public RefToken CreatedBy { get; set; } + + [BsonRequired] + [BsonElement("mb")] + public RefToken LastModifiedBy { get; set; } + + [BsonIgnore] + public NamedContentData? Data + { + get { return data; } + } + + [BsonIgnore] + public NamedContentData DataDraft + { + get { return dataDraft; } + } + + public void ParseData(Schema schema, IJsonSerializer serializer) + { + data = DataByIds?.FromMongoModel(schema, ReferencedIdsDeleted, serializer); + + if (DataDraftByIds != null) + { + dataDraft = DataDraftByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs new file mode 100644 index 000000000..402b89427 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -0,0 +1,156 @@ +// ========================================================================== +// 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.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public partial class MongoContentRepository : IContentRepository, IInitializable + { + private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>(); + private readonly IAppProvider appProvider; + private readonly IJsonSerializer serializer; + private readonly ITextIndexer indexer; + private readonly string typeAssetDeleted; + private readonly string typeContentDeleted; + private readonly MongoContentCollection contents; + + static MongoContentRepository() + { + StatusSerializer.Register(); + } + + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(appProvider); + Guard.NotNull(serializer); + Guard.NotNull(indexer); + Guard.NotNull(typeNameRegistry); + + this.appProvider = appProvider; + this.indexer = indexer; + this.serializer = serializer; + + typeAssetDeleted = typeNameRegistry.GetName(); + typeContentDeleted = typeNameRegistry.GetName(); + + contents = new MongoContentCollection(database, serializer, appProvider); + } + + public Task InitializeAsync(CancellationToken ct = default) + { + return contents.InitializeAsync(ct); + } + + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, bool inDraft, ClrQuery query, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(schema); + Guard.NotNull(query); + + using (Profiler.TraceMethod("QueryAsyncByQuery")) + { + var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); + + if (fullTextIds?.Count == 0) + { + return ResultList.CreateFrom(0); + } + + return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft); + } + } + + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet ids, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(ids); + Guard.NotNull(schema); + + using (Profiler.TraceMethod("QueryAsyncByIds")) + { + return await contents.QueryAsync(schema, ids, status, includeDraft); + } + } + + public async Task> QueryAsync(IAppEntity app, Status[]? status, HashSet ids, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(ids); + + using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) + { + return await contents.QueryAsync(app, ids, status, includeDraft); + } + } + + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft = true) + { + Guard.NotNull(app); + Guard.NotNull(schema); + + using (Profiler.TraceMethod()) + { + return await contents.FindContentAsync(schema, id, status, includeDraft); + } + } + + public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + using (Profiler.TraceMethod()) + { + await contents.QueryScheduledWithoutDataAsync(now, callback); + } + } + + public async Task> QueryIdsAsync(Guid appId, HashSet ids) + { + using (Profiler.TraceMethod()) + { + return await contents.QueryIdsAsync(ids); + } + } + + public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) + { + using (Profiler.TraceMethod()) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId); + + if (schema == null) + { + return EmptyIds; + } + + return await contents.QueryIdsAsync(schema, filterNode); + } + } + + public Task ClearAsync() + { + return contents.ClearAsync(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs new file mode 100644 index 000000000..c9196b081 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public partial class MongoContentRepository : ISnapshotStore + { + async Task ISnapshotStore.RemoveAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + await contents.RemoveAsync(key); + } + } + + async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + { + using (Profiler.TraceMethod()) + { + await contents.ReadAllAsync(callback, GetSchemaAsync, ct); + } + } + + async Task<(ContentState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + return await contents.ReadAsync(key, GetSchemaAsync); + } + } + + async Task ISnapshotStore.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod()) + { + if (value.SchemaId.Id == Guid.Empty) + { + return; + } + + var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id); + + var idData = value.Data!.ToMongoModel(schema.SchemaDef, serializer); + var idDraftData = idData; + + if (!ReferenceEquals(value.Data, value.DataDraft)) + { + idDraftData = value.DataDraft.ToMongoModel(schema.SchemaDef, serializer); + } + + var content = SimpleMapper.Map(value, new MongoContentEntity + { + DataByIds = idData, + DataDraftByIds = idDraftData, + IsDeleted = value.IsDeleted, + IndexedAppId = value.AppId.Id, + IndexedSchemaId = value.SchemaId.Id, + ReferencedIds = idData.ToReferencedIds(schema.SchemaDef), + ScheduledAt = value.ScheduleJob?.DueTime, + Version = newVersion + }); + + await contents.UpsertAsync(content, oldVersion); + } + } + + private async Task GetSchemaAsync(Guid appId, Guid schemaId) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId, true); + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(ISchemaEntity)); + } + + return schema; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs new file mode 100644 index 000000000..95efcb736 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// 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 MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors +{ + public static class FilterFactory + { + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + + public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft) + { + var pathConverter = Adapt.Path(schema, useDraft); + + if (query.Filter != null) + { + query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter)); + } + + query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList(); + + return query; + } + + public static FilterNode? AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft) + { + var pathConverter = Adapt.Path(schema, useDraft); + + return filterNode.Accept(new AdaptionVisitor(pathConverter)); + } + + public static IFindFluent ContentSort(this IFindFluent cursor, ClrQuery query) + { + return cursor.Sort(query.BuildSort()); + } + + public static IFindFluent ContentTake(this IFindFluent cursor, ClrQuery query) + { + return cursor.Take(query); + } + + public static IFindFluent ContentSkip(this IFindFluent cursor, ClrQuery query) + { + return cursor.Skip(query); + } + + public static IFindFluent WithoutDraft(this IFindFluent cursor, bool includeDraft) + { + return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; + } + + public static FilterDefinition Build(Guid schemaId, Guid id, Status[]? status) + { + return CreateFilter(null, schemaId, new List { id }, status, null); + } + + public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[]? status) + { + return CreateFilter(appId, null, ids, status, null); + } + + public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[]? status) + { + return CreateFilter(null, schemaId, ids, status, null); + } + + public static FilterDefinition ToFilter(this ClrQuery query, Guid schemaId, ICollection? ids, Status[]? status) + { + return CreateFilter(null, schemaId, ids, status, query); + } + + private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection? ids, Status[]? status, + ClrQuery? query) + { + var filters = new List>(); + + if (appId.HasValue) + { + filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); + } + + if (schemaId.HasValue) + { + filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); + } + + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + + if (status != null) + { + filters.Add(Filter.In(x => x.Status, status)); + } + + if (ids != null && ids.Count > 0) + { + if (ids.Count > 1) + { + filters.Add(Filter.In(x => x.Id, ids)); + } + else + { + filters.Add(Filter.Eq(x => x.Id, ids.First())); + } + } + + if (query?.Filter != null) + { + filters.Add(query.Filter.BuildFilter()); + } + + return Filter.And(filters); + } + + public static FilterDefinition ToFilter(this FilterNode filterNode, Guid schemaId) + { + var filters = new List> + { + Filter.Eq(x => x.IndexedSchemaId, schemaId), + Filter.Ne(x => x.IsDeleted, true), + filterNode.BuildFilter() + }; + + return Filter.And(filters); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs new file mode 100644 index 000000000..c5a417aef --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.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 MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity + { + [BsonRequired] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid AppId { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid RuleId { get; set; } + + [BsonRequired] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public RuleResult Result { get; set; } + + [BsonRequired] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public RuleJobResult JobResult { get; set; } + + [BsonRequired] + [BsonElement] + [BsonJson] + public RuleJob Job { get; set; } + + [BsonRequired] + [BsonElement] + public string? LastDump { get; set; } + + [BsonRequired] + [BsonElement] + public int NumCalls { get; set; } + + [BsonRequired] + [BsonElement] + public Instant Expires { get; set; } + + [BsonRequired] + [BsonElement] + public Instant? NextAttempt { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs new file mode 100644 index 000000000..1deb73603 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// 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.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository + { + private readonly MongoRuleStatisticsCollection statisticsCollection; + + public MongoRuleEventRepository(IMongoDatabase database) + : base(database) + { + statisticsCollection = new MongoRuleStatisticsCollection(database); + } + + protected override string CollectionName() + { + return "RuleEvents"; + } + + protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + await statisticsCollection.InitializeAsync(ct); + + await collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel(Index.Ascending(x => x.NextAttempt)), + new CreateIndexModel(Index.Ascending(x => x.AppId).Descending(x => x.Created)), + new CreateIndexModel( + Index + .Ascending(x => x.Expires), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }) + }, ct); + } + + public Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default) + { + return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); + } + + public async Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20) + { + var filter = Filter.Eq(x => x.AppId, appId); + + if (ruleId.HasValue) + { + filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId)); + } + + var ruleEventEntities = + await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created) + .ToListAsync(); + + return ruleEventEntities; + } + + public async Task FindAsync(Guid id) + { + var ruleEvent = + await Collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); + + return ruleEvent; + } + + public async Task CountByAppAsync(Guid appId) + { + return (int)await Collection.CountDocumentsAsync(x => x.AppId == appId); + } + + public Task EnqueueAsync(Guid id, Instant nextAttempt) + { + return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); + } + + public Task EnqueueAsync(RuleJob job, Instant nextAttempt) + { + var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); + + return Collection.InsertOneIfNotExistsAsync(entity); + } + + public Task CancelAsync(Guid id) + { + return Collection.UpdateOneAsync(x => x.Id == id, + Update + .Set(x => x.NextAttempt, null) + .Set(x => x.JobResult, RuleJobResult.Cancelled)); + } + + public async Task MarkSentAsync(RuleJob job, string? dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall) + { + if (result == RuleResult.Success) + { + await statisticsCollection.IncrementSuccess(job.AppId, job.RuleId, finished); + } + else + { + await statisticsCollection.IncrementFailed(job.AppId, job.RuleId, finished); + } + + await Collection.UpdateOneAsync(x => x.Id == job.Id, + Update + .Set(x => x.Result, result) + .Set(x => x.LastDump, dump) + .Set(x => x.JobResult, jobResult) + .Set(x => x.NextAttempt, nextCall) + .Inc(x => x.NumCalls, 1)); + } + + public Task> QueryStatisticsByAppAsync(Guid appId) + { + return statisticsCollection.QueryByAppAsync(appId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj new file mode 100644 index 000000000..35af53605 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -0,0 +1,32 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs new file mode 100644 index 000000000..239bfcaad --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class AppProvider : IAppProvider + { + private readonly ILocalCache localCache; + private readonly IAppsIndex indexForApps; + private readonly IRulesIndex indexRules; + private readonly ISchemasIndex indexSchemas; + + public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas) + { + Guard.NotNull(indexForApps); + Guard.NotNull(indexRules); + Guard.NotNull(indexSchemas); + Guard.NotNull(localCache); + + this.localCache = localCache; + this.indexForApps = indexForApps; + this.indexRules = indexRules; + this.indexSchemas = indexSchemas; + } + + public Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(Guid appId, Guid id) + { + return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => + { + return await GetAppWithSchemaUncachedAsync(appId, id); + }); + } + + private async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaUncachedAsync(Guid appId, Guid id) + { + var app = await GetAppAsync(appId); + + if (app == null) + { + return (null, null); + } + + var schema = await GetSchemaAsync(appId, id, false); + + if (schema == null) + { + return (null, null); + } + + return (app, schema); + } + + public Task GetAppAsync(Guid appId) + { + return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () => + { + return await indexForApps.GetAppAsync(appId); + }); + } + + public Task GetAppAsync(string appName) + { + return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => + { + return await indexForApps.GetAppByNameAsync(appName); + }); + } + + public Task> GetUserAppsAsync(string userId, PermissionSet permissions) + { + return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => + { + return await indexForApps.GetAppsForUserAsync(userId, permissions); + }); + } + + public Task GetSchemaAsync(Guid appId, string name) + { + return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => + { + return await indexSchemas.GetSchemaByNameAsync(appId, name); + }); + } + + public Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) + { + return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => + { + return await indexSchemas.GetSchemaAsync(appId, id, allowDeleted); + }); + } + + public Task> GetSchemasAsync(Guid appId) + { + return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => + { + return await indexSchemas.GetSchemasAsync(appId); + }); + } + + public Task> GetRulesAsync(Guid appId) + { + return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => + { + return await indexRules.GetRulesAsync(appId); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs new file mode 100644 index 000000000..0359af415 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppCommandMiddleware : GrainCommandMiddleware + { + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IContextProvider contextProvider; + + public AppCommandMiddleware( + IGrainFactory grainFactory, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IContextProvider contextProvider) + : base(grainFactory) + { + Guard.NotNull(contextProvider); + Guard.NotNull(assetStore); + Guard.NotNull(assetThumbnailGenerator); + + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.contextProvider = contextProvider; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is UploadAppImage uploadImage) + { + await UploadAsync(uploadImage); + } + + await ExecuteCommandAsync(context); + + if (context.PlainResult is IAppEntity app) + { + contextProvider.Context.App = app; + } + + await next(); + } + + private async Task UploadAsync(UploadAppImage uploadImage) + { + var file = uploadImage.File; + + var image = await assetThumbnailGenerator.GetImageInfoAsync(file.OpenRead()); + + if (image == null) + { + throw new ValidationException("File is not an image."); + } + + await assetStore.UploadAsync(uploadImage.AppId.ToString(), file.OpenRead(), true); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs new file mode 100644 index 000000000..5e842433d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -0,0 +1,510 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Guards; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppGrain : DomainObjectGrain, IAppGrain + { + private readonly InitialPatterns initialPatterns; + private readonly IAppPlansProvider appPlansProvider; + private readonly IAppPlanBillingManager appPlansBillingManager; + private readonly IUserResolver userResolver; + + public AppGrain( + InitialPatterns initialPatterns, + IStore store, + ISemanticLog log, + IAppPlansProvider appPlansProvider, + IAppPlanBillingManager appPlansBillingManager, + IUserResolver userResolver) + : base(store, log) + { + Guard.NotNull(initialPatterns); + Guard.NotNull(userResolver); + Guard.NotNull(appPlansProvider); + Guard.NotNull(appPlansBillingManager); + + this.userResolver = userResolver; + this.appPlansProvider = appPlansProvider; + this.appPlansBillingManager = appPlansBillingManager; + this.initialPatterns = initialPatterns; + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotArchived(); + + switch (command) + { + case CreateApp createApp: + return CreateReturn(createApp, c => + { + GuardApp.CanCreate(c); + + Create(c); + + return Snapshot; + }); + + case UpdateApp updateApp: + return UpdateReturn(updateApp, c => + { + GuardApp.CanUpdate(c); + + Update(c); + + return Snapshot; + }); + + case UploadAppImage uploadImage: + return UpdateReturn(uploadImage, c => + { + GuardApp.CanUploadImage(c); + + UploadImage(c); + + return Snapshot; + }); + + case RemoveAppImage removeImage: + return UpdateReturn(removeImage, c => + { + GuardApp.CanRemoveImage(c); + + RemoveImage(c); + + return Snapshot; + }); + + case AssignContributor assignContributor: + return UpdateReturnAsync(assignContributor, async c => + { + await GuardAppContributors.CanAssign(Snapshot.Contributors, Snapshot.Roles, c, userResolver, GetPlan()); + + AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); + + return Snapshot; + }); + + case RemoveContributor removeContributor: + return UpdateReturn(removeContributor, c => + { + GuardAppContributors.CanRemove(Snapshot.Contributors, c); + + RemoveContributor(c); + + return Snapshot; + }); + + case AttachClient attachClient: + return UpdateReturn(attachClient, c => + { + GuardAppClients.CanAttach(Snapshot.Clients, c); + + AttachClient(c); + + return Snapshot; + }); + + case UpdateClient updateClient: + return UpdateReturn(updateClient, c => + { + GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles); + + UpdateClient(c); + + return Snapshot; + }); + + case RevokeClient revokeClient: + return UpdateReturn(revokeClient, c => + { + GuardAppClients.CanRevoke(Snapshot.Clients, c); + + RevokeClient(c); + + return Snapshot; + }); + + case AddWorkflow addWorkflow: + return UpdateReturn(addWorkflow, c => + { + GuardAppWorkflows.CanAdd(c); + + AddWorkflow(c); + + return Snapshot; + }); + + case UpdateWorkflow updateWorkflow: + return UpdateReturn(updateWorkflow, c => + { + GuardAppWorkflows.CanUpdate(Snapshot.Workflows, c); + + UpdateWorkflow(c); + + return Snapshot; + }); + + case DeleteWorkflow deleteWorkflow: + return UpdateReturn(deleteWorkflow, c => + { + GuardAppWorkflows.CanDelete(Snapshot.Workflows, c); + + DeleteWorkflow(c); + + return Snapshot; + }); + + case AddLanguage addLanguage: + return UpdateReturn(addLanguage, c => + { + GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c); + + AddLanguage(c); + + return Snapshot; + }); + + case RemoveLanguage removeLanguage: + return UpdateReturn(removeLanguage, c => + { + GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c); + + RemoveLanguage(c); + + return Snapshot; + }); + + case UpdateLanguage updateLanguage: + return UpdateReturn(updateLanguage, c => + { + GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c); + + UpdateLanguage(c); + + return Snapshot; + }); + + case AddRole addRole: + return UpdateReturn(addRole, c => + { + GuardAppRoles.CanAdd(Snapshot.Roles, c); + + AddRole(c); + + return Snapshot; + }); + + case DeleteRole deleteRole: + return UpdateReturn(deleteRole, c => + { + GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients); + + DeleteRole(c); + + return Snapshot; + }); + + case UpdateRole updateRole: + return UpdateReturn(updateRole, c => + { + GuardAppRoles.CanUpdate(Snapshot.Roles, c); + + UpdateRole(c); + + return Snapshot; + }); + + case AddPattern addPattern: + return UpdateReturn(addPattern, c => + { + GuardAppPatterns.CanAdd(Snapshot.Patterns, c); + + AddPattern(c); + + return Snapshot; + }); + + case DeletePattern deletePattern: + return UpdateReturn(deletePattern, c => + { + GuardAppPatterns.CanDelete(Snapshot.Patterns, c); + + DeletePattern(c); + + return Snapshot; + }); + + case UpdatePattern updatePattern: + return UpdateReturn(updatePattern, c => + { + GuardAppPatterns.CanUpdate(Snapshot.Patterns, c); + + UpdatePattern(c); + + return Snapshot; + }); + + case ChangePlan changePlan: + return UpdateReturnAsync(changePlan, async c => + { + GuardApp.CanChangePlan(c, Snapshot.Plan, appPlansProvider); + + if (c.FromCallback) + { + ChangePlan(c); + + return null; + } + else + { + var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), c.PlanId); + + switch (result) + { + case PlanChangedResult _: + ChangePlan(c); + break; + case PlanResetResult _: + ResetPlan(c); + break; + } + + return result; + } + }); + + case ArchiveApp archiveApp: + return UpdateAsync(archiveApp, async c => + { + await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null); + + ArchiveApp(c); + }); + + default: + throw new NotSupportedException(); + } + } + + private IAppLimitsPlan? GetPlan() + { + return appPlansProvider.GetPlan(Snapshot.Plan?.PlanId); + } + + public void Create(CreateApp command) + { + var appId = NamedId.Of(command.AppId, command.Name); + + var events = new List + { + CreateInitalEvent(command.Name), + CreateInitialOwner(command.Actor), + CreateInitialLanguage() + }; + + foreach (var pattern in initialPatterns) + { + events.Add(CreateInitialPattern(pattern.Key, pattern.Value)); + } + + foreach (var @event in events) + { + @event.Actor = command.Actor; + @event.AppId = appId; + + RaiseEvent(@event); + } + } + + public void UpdateClient(UpdateClient command) + { + if (!string.IsNullOrWhiteSpace(command.Name)) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); + } + + if (command.Role != null) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role })); + } + } + + public void Update(UpdateApp command) + { + RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); + } + + public void UploadImage(UploadAppImage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); + } + + public void RemoveImage(RemoveAppImage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppImageRemoved())); + } + + public void UpdateLanguage(UpdateLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); + } + + public void AssignContributor(AssignContributor command, bool isAdded) + { + RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { IsAdded = isAdded })); + } + + public void RemoveContributor(RemoveContributor command) + { + RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); + } + + public void AttachClient(AttachClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); + } + + public void RevokeClient(RevokeClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); + } + + public void AddWorkflow(AddWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); + } + + public void UpdateWorkflow(UpdateWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); + } + + public void DeleteWorkflow(DeleteWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); + } + + public void AddLanguage(AddLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); + } + + public void RemoveLanguage(RemoveLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); + } + + public void ChangePlan(ChangePlan command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); + } + + public void ResetPlan(ChangePlan command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPlanReset())); + } + + public void AddPattern(AddPattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); + } + + public void DeletePattern(DeletePattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); + } + + public void UpdatePattern(UpdatePattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); + } + + public void AddRole(AddRole command) + { + RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded())); + } + + public void DeleteRole(DeleteRole command) + { + RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted())); + } + + public void UpdateRole(UpdateRole command) + { + RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated())); + } + + public void ArchiveApp(ArchiveApp command) + { + RaiseEvent(SimpleMapper.Map(command, new AppArchived())); + } + + private void VerifyNotArchived() + { + if (Snapshot.IsArchived) + { + throw new DomainException("App has already been archived."); + } + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = NamedId.Of(Snapshot.Id, Snapshot.Name); + } + + RaiseEvent(Envelope.Create(@event)); + } + + private static AppCreated CreateInitalEvent(string name) + { + return new AppCreated { Name = name }; + } + + private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern) + { + return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message }; + } + + private static AppLanguageAdded CreateInitialLanguage() + { + return new AppLanguageAdded { Language = Language.EN }; + } + + private static AppContributorAssigned CreateInitialOwner(RefToken actor) + { + return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; + } + + public Task> GetStateAsync() + { + return J.AsTask(Snapshot); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs new file mode 100644 index 000000000..ed1487a9b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -0,0 +1,161 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppHistoryEventsCreator : HistoryEventsCreatorBase + { + public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "assigned {user:[Contributor]} as {[Role]}"); + + AddEventMessage( + "removed {user:[Contributor]} from app"); + + AddEventMessage( + "added client {[Id]} to app"); + + AddEventMessage( + "revoked client {[Id]}"); + + AddEventMessage( + "updated client {[Id]}"); + + AddEventMessage( + "renamed client {[Id]} to {[Name]}"); + + AddEventMessage( + "changed plan to {[Plan]}"); + + AddEventMessage( + "resetted plan"); + + AddEventMessage( + "added language {[Language]}"); + + AddEventMessage( + "removed language {[Language]}"); + + AddEventMessage( + "updated language {[Language]}"); + + AddEventMessage( + "changed master language to {[Language]}"); + + AddEventMessage( + "added pattern {[Name]}"); + + AddEventMessage( + "deleted pattern {[PatternId]}"); + + AddEventMessage( + "updated pattern {[Name]}"); + + AddEventMessage( + "added role {[Name]}"); + + AddEventMessage( + "deleted role {[Name]}"); + + AddEventMessage( + "updated role {[Name]}"); + } + + private HistoryEvent? CreateEvent(IEvent @event) + { + switch (@event) + { + case AppContributorAssigned e: + return CreateContributorsEvent(e, e.ContributorId, e.Role); + case AppContributorRemoved e: + return CreateContributorsEvent(e, e.ContributorId); + case AppClientAttached e: + return CreateClientsEvent(e, e.Id); + case AppClientRenamed e: + return CreateClientsEvent(e, e.Id, ClientName(e)); + case AppClientRevoked e: + return CreateClientsEvent(e, e.Id); + case AppLanguageAdded e: + return CreateLanguagesEvent(e, e.Language); + case AppLanguageUpdated e: + return CreateLanguagesEvent(e, e.Language); + case AppMasterLanguageSet e: + return CreateLanguagesEvent(e, e.Language); + case AppLanguageRemoved e: + return CreateLanguagesEvent(e, e.Language); + case AppPatternAdded e: + return CreatePatternsEvent(e, e.PatternId, e.Name); + case AppPatternUpdated e: + return CreatePatternsEvent(e, e.PatternId, e.Name); + case AppPatternDeleted e: + return CreatePatternsEvent(e, e.PatternId); + case AppRoleAdded e: + return CreateRolesEvent(e, e.Name); + case AppRoleUpdated e: + return CreateRolesEvent(e, e.Name); + case AppRoleDeleted e: + return CreateRolesEvent(e, e.Name); + case AppPlanChanged e: + return CreatePlansEvent(e, e.PlanId); + case AppPlanReset e: + return CreatePlansEvent(e); + } + + return null; + } + + private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) + { + return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); + } + + private HistoryEvent CreateLanguagesEvent(IEvent e, Language language) + { + return ForEvent(e, "settings.languages").Param("Language", language); + } + + private HistoryEvent CreateRolesEvent(IEvent e, string name) + { + return ForEvent(e, "settings.roles").Param("Name", name); + } + + private HistoryEvent CreatePatternsEvent(IEvent e, Guid id, string? name = null) + { + return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name); + } + + private HistoryEvent CreateClientsEvent(IEvent e, string id, string? name = null) + { + return ForEvent(e, "settings.clients").Param("Id", id).Param("Name", name); + } + + private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) + { + return ForEvent(e, "settings.plan").Param("Plan", plan); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + return Task.FromResult(CreateEvent(@event.Payload)); + } + + private static string ClientName(AppClientRenamed e) + { + return !string.IsNullOrWhiteSpace(e.Name) ? e.Name : e.Id; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs new file mode 100644 index 000000000..9975c80ad --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppUISettings : IAppUISettings + { + private readonly IGrainFactory grainFactory; + + public AppUISettings(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid appId, string? userId) + { + var result = await GetGrain(appId, userId).GetAsync(); + + return result.Value; + } + + public Task RemoveAsync(Guid appId, string? userId, string path) + { + return GetGrain(appId, userId).RemoveAsync(path); + } + + public Task SetAsync(Guid appId, string? userId, string path, IJsonValue value) + { + return GetGrain(appId, userId).SetAsync(path, value.AsJ()); + } + + public Task SetAsync(Guid appId, string? userId, JsonObject settings) + { + return GetGrain(appId, userId).SetAsync(settings.AsJ()); + } + + private IAppUISettingsGrain GetGrain(Guid appId, string? userId) + { + return grainFactory.GetGrain(Key(appId, userId)); + } + + private string Key(Guid appId, string? userId) + { + if (!string.IsNullOrWhiteSpace(userId)) + { + return $"{appId}_{userId}"; + } + else + { + return $"{appId}"; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs new file mode 100644 index 000000000..25952d89e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain + { + private readonly IGrainState state; + + [CollectionName("UISettings")] + public sealed class GrainState + { + public JsonObject Settings { get; set; } = JsonValue.Object(); + } + + public AppUISettingsGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task> GetAsync() + { + return Task.FromResult(state.Value.Settings.AsJ()); + } + + public Task SetAsync(J settings) + { + state.Value.Settings = settings; + + return state.WriteAsync(); + } + + public Task SetAsync(string path, J value) + { + var container = GetContainer(path, true, out var key); + + if (container == null) + { + throw new InvalidOperationException("Path does not lead to an object."); + } + + container[key] = value.Value; + + return state.WriteAsync(); + } + + public async Task RemoveAsync(string path) + { + var container = GetContainer(path, false, out var key); + + if (container?.ContainsKey(key) == true) + { + container.Remove(key); + + await state.WriteAsync(); + } + } + + private JsonObject? GetContainer(string path, bool add, out string key) + { + Guard.NotNullOrEmpty(path); + + var segments = path.Split('.'); + + key = segments[segments.Length - 1]; + + var current = state.Value.Settings; + + if (segments.Length > 1) + { + foreach (var segment in segments.Take(segments.Length - 1)) + { + if (!current.TryGetValue(segment, out var temp)) + { + if (add) + { + temp = JsonValue.Object(); + + current[segment] = temp; + } + else + { + return null; + } + } + + if (temp is JsonObject next) + { + current = next; + } + else + { + return null; + } + } + } + + return current; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs new file mode 100644 index 000000000..4cc3c058b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -0,0 +1,204 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class BackupApps : BackupHandler + { + private const string UsersFile = "Users.json"; + private const string SettingsFile = "Settings.json"; + private readonly IAppUISettings appUISettings; + private readonly IAppsIndex appsIndex; + private readonly IUserResolver userResolver; + private readonly HashSet contributors = new HashSet(); + private readonly Dictionary userMapping = new Dictionary(); + private Dictionary usersWithEmail = new Dictionary(); + private string? appReservation; + private string appName; + + public override string Name { get; } = "Apps"; + + public BackupApps(IAppUISettings appUISettings, IAppsIndex appsIndex, IUserResolver userResolver) + { + Guard.NotNull(appsIndex); + Guard.NotNull(appUISettings); + Guard.NotNull(userResolver); + + this.appsIndex = appsIndex; + this.appUISettings = appUISettings; + this.userResolver = userResolver; + } + + public override async Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) + { + if (@event.Payload is AppContributorAssigned appContributorAssigned) + { + var userId = appContributorAssigned.ContributorId; + + if (!usersWithEmail.ContainsKey(userId)) + { + var user = await userResolver.FindByIdOrEmailAsync(userId); + + if (user != null) + { + usersWithEmail.Add(userId, user.Email); + } + } + } + } + + public override async Task BackupAsync(Guid appId, BackupWriter writer) + { + await WriteUsersAsync(writer); + await WriteSettingsAsync(writer, appId); + } + + public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case AppCreated appCreated: + { + appName = appCreated.Name; + + await ResolveUsersAsync(reader); + await ReserveAppAsync(appId); + + break; + } + + case AppContributorAssigned contributorAssigned: + { + if (!userMapping.TryGetValue(contributorAssigned.ContributorId, out var user) || user.Equals(actor)) + { + return false; + } + + contributorAssigned.ContributorId = user.Identifier; + contributors.Add(contributorAssigned.ContributorId); + break; + } + + case AppContributorRemoved contributorRemoved: + { + if (!userMapping.TryGetValue(contributorRemoved.ContributorId, out var user) || user.Equals(actor)) + { + return false; + } + + contributorRemoved.ContributorId = user.Identifier; + contributors.Remove(contributorRemoved.ContributorId); + break; + } + } + + if (@event.Payload is SquidexEvent squidexEvent) + { + squidexEvent.Actor = MapUser(squidexEvent.Actor.Identifier, actor); + } + + return true; + } + + public override Task RestoreAsync(Guid appId, BackupReader reader) + { + return ReadSettingsAsync(reader, appId); + } + + private async Task ReserveAppAsync(Guid appId) + { + appReservation = await appsIndex.ReserveAsync(appId, appName); + + if (appReservation == null) + { + throw new BackupRestoreException("The app id or name is not available."); + } + } + + public override async Task CleanupRestoreErrorAsync(Guid appId) + { + if (appReservation != null) + { + await appsIndex.RemoveReservationAsync(appReservation); + } + } + + private RefToken MapUser(string userId, RefToken fallback) + { + return userMapping.GetOrAdd(userId, fallback); + } + + private async Task ResolveUsersAsync(BackupReader reader) + { + await ReadUsersAsync(reader); + + foreach (var kvp in usersWithEmail) + { + var email = kvp.Value; + + var user = await userResolver.FindByIdOrEmailAsync(email); + + if (user == null && await userResolver.CreateUserIfNotExists(kvp.Value)) + { + user = await userResolver.FindByIdOrEmailAsync(email); + } + + if (user != null) + { + userMapping[kvp.Key] = new RefToken(RefTokenType.Subject, user.Id); + } + } + } + + private async Task ReadUsersAsync(BackupReader reader) + { + var json = await reader.ReadJsonAttachmentAsync>(UsersFile); + + usersWithEmail = json; + } + + private async Task WriteUsersAsync(BackupWriter writer) + { + var json = usersWithEmail; + + await writer.WriteJsonAsync(UsersFile, json); + } + + private async Task WriteSettingsAsync(BackupWriter writer, Guid appId) + { + var json = await appUISettings.GetAsync(appId, null); + + await writer.WriteJsonAsync(SettingsFile, json); + } + + private async Task ReadSettingsAsync(BackupReader reader, Guid appId) + { + var json = await reader.ReadJsonAttachmentAsync(SettingsFile); + + await appUISettings.SetAsync(appId, null, json); + } + + public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) + { + await appsIndex.AddAsync(appReservation); + + await appsIndex.RebuildByContributorsAsync(appId, contributors); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs new file mode 100644 index 000000000..9f74f6291 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AddPattern : AppCommand + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + + public AddPattern() + { + PatternId = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs new file mode 100644 index 000000000..f47952812 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdateApp : AppCommand + { + public string? Label { get; set; } + + public string? Description { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs new file mode 100644 index 000000000..0f28418f9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdatePattern : AppCommand + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs new file mode 100644 index 000000000..a0f8e9142 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class DefaultAppLogStore : IAppLogStore + { + private readonly ILogStore logStore; + + public DefaultAppLogStore(ILogStore logStore) + { + Guard.NotNull(logStore); + + this.logStore = logStore; + } + + public Task ReadLogAsync(string appId, DateTime from, DateTime to, Stream stream) + { + Guard.NotNull(appId); + + return logStore.ReadLogAsync(appId, from, to, stream); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs new file mode 100644 index 000000000..a70ca3f61 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics +{ + public sealed class OrleansAppsHealthCheck : IHealthCheck + { + private readonly IAppsByNameIndexGrain index; + + public OrleansAppsHealthCheck(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + index = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + await index.CountAsync(); + + return HealthCheckResult.Healthy("Orleans must establish communication."); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs new file mode 100644 index 000000000..02239fde6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardApp + { + public static void CanCreate(CreateApp command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot create app.", e => + { + if (!command.Name.IsSlug()) + { + e(Not.ValidSlug("Name"), nameof(command.Name)); + } + }); + } + + public static void CanUploadImage(UploadAppImage command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot upload image.", e => + { + if (command.File == null) + { + e(Not.Defined("File"), nameof(command.File)); + } + }); + } + + public static void CanUpdate(UpdateApp command) + { + Guard.NotNull(command); + } + + public static void CanRemoveImage(RemoveAppImage command) + { + Guard.NotNull(command); + } + + public static void CanChangePlan(ChangePlan command, AppPlan? plan, IAppPlansProvider appPlans) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot change plan.", e => + { + if (string.IsNullOrWhiteSpace(command.PlanId)) + { + e(Not.Defined("Plan id"), nameof(command.PlanId)); + return; + } + + if (appPlans.GetPlan(command.PlanId) == null) + { + e("A plan with this id does not exist.", nameof(command.PlanId)); + } + + if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) + { + e("Plan can only changed from the user who configured the plan initially."); + } + + if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) + { + e("App has already this plan."); + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs new file mode 100644 index 000000000..a14c9cffe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppClients + { + public static void CanAttach(AppClients clients, AttachClient command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot attach client.", e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("Client id"), nameof(command.Id)); + } + else if (clients.ContainsKey(command.Id)) + { + e("A client with the same id already exists."); + } + }); + } + + public static void CanRevoke(AppClients clients, RevokeClient command) + { + Guard.NotNull(command); + + GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot revoke client.", e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("Client id"), nameof(command.Id)); + } + }); + } + + public static void CanUpdate(AppClients clients, UpdateClient command, Roles roles) + { + Guard.NotNull(command); + + var client = GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot update client.", e => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + e(Not.Defined("Client id"), nameof(command.Id)); + } + + if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null) + { + e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); + } + + if (command.Role != null && !roles.Contains(command.Role)) + { + e(Not.Valid("role"), nameof(command.Role)); + } + + if (client == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) + { + e(Not.New("Client", "name"), nameof(command.Name)); + } + + if (command.Role == client.Role) + { + e(Not.New("Client", "role"), nameof(command.Role)); + } + }); + } + + private static AppClient? GetClientOrThrow(AppClients clients, string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return null; + } + + if (!clients.TryGetValue(id, out var client)) + { + throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity)); + } + + return client; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs new file mode 100644 index 000000000..88c5240a0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.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.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppContributors + { + public static Task CanAssign(AppContributors contributors, Roles roles, AssignContributor command, IUserResolver users, IAppLimitsPlan? plan) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot assign contributor.", async e => + { + if (!roles.Contains(command.Role)) + { + e(Not.Valid("role"), nameof(command.Role)); + } + + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined("Contributor id"), nameof(command.ContributorId)); + } + else + { + var user = await users.FindByIdOrEmailAsync(command.ContributorId); + + if (user == null) + { + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); + } + + command.ContributorId = user.Id; + + if (!command.IsRestore) + { + if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) + { + throw new DomainForbiddenException("You cannot change your own role."); + } + + if (contributors.TryGetValue(command.ContributorId, out var role)) + { + if (role == command.Role) + { + e(Not.New("Contributor", "role"), nameof(command.Role)); + } + } + else + { + if (plan != null && plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) + { + e("You have reached the maximum number of contributors for your plan."); + } + } + } + } + }); + } + + public static void CanRemove(AppContributors contributors, RemoveContributor command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot remove contributor.", e => + { + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined("Contributor id"), nameof(command.ContributorId)); + } + + var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); + + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) + { + e("Cannot remove the only owner."); + } + }); + + if (!contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs new file mode 100644 index 000000000..f924941dd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppLanguages + { + public static void CanAdd(LanguagesConfig languages, AddLanguage command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add language.", e => + { + if (command.Language == null) + { + e(Not.Defined("Language code"), nameof(command.Language)); + } + else if (languages.Contains(command.Language)) + { + e("Language has already been added."); + } + }); + } + + public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) + { + Guard.NotNull(command); + + var config = GetConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot remove language.", e => + { + if (command.Language == null) + { + e(Not.Defined("Language code"), nameof(command.Language)); + } + + if (languages.Master == config) + { + e("Master language cannot be removed."); + } + }); + } + + public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) + { + Guard.NotNull(command); + + var config = GetConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot update language.", e => + { + if (command.Language == null) + { + e(Not.Defined("Language code"), nameof(command.Language)); + } + + if ((languages.Master == config || command.IsMaster) && command.IsOptional) + { + e("Master language cannot be made optional.", nameof(command.IsMaster)); + } + + if (command.Fallback == null) + { + return; + } + + foreach (var fallback in command.Fallback) + { + if (!languages.Contains(fallback)) + { + e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback)); + } + } + }); + } + + private static LanguageConfig? GetConfigOrThrow(LanguagesConfig languages, Language language) + { + if (language == null) + { + return null; + } + + if (!languages.TryGetConfig(language, out var languageConfig)) + { + throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity)); + } + + return languageConfig; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs new file mode 100644 index 000000000..0cdbe18f0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppPatterns + { + public static void CanAdd(AppPatterns patterns, AddPattern command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add pattern.", e => + { + if (command.PatternId == Guid.Empty) + { + e(Not.Defined("Id"), nameof(command.PatternId)); + } + + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + + if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("A pattern with the same name already exists."); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + e(Not.Defined("Pattern"), nameof(command.Pattern)); + } + else if (!command.Pattern.IsValidRegex()) + { + e(Not.Valid("Pattern"), nameof(command.Pattern)); + } + + if (patterns.Values.Any(x => x.Pattern == command.Pattern)) + { + e("This pattern already exists but with another name."); + } + }); + } + + public static void CanDelete(AppPatterns patterns, DeletePattern command) + { + Guard.NotNull(command); + + if (!patterns.ContainsKey(command.PatternId)) + { + throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); + } + } + + public static void CanUpdate(AppPatterns patterns, UpdatePattern command) + { + Guard.NotNull(command); + + if (!patterns.ContainsKey(command.PatternId)) + { + throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); + } + + Validate.It(() => "Cannot update pattern.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + + if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("A pattern with the same name already exists."); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + e(Not.Defined("Pattern"), nameof(command.Pattern)); + } + else if (!command.Pattern.IsValidRegex()) + { + e(Not.Valid("Pattern"), nameof(command.Pattern)); + } + + if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern)) + { + e("This pattern already exists but with another name."); + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs new file mode 100644 index 000000000..929bd0692 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppRoles + { + public static void CanAdd(Roles roles, AddRole command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add role.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + else if (roles.Contains(command.Name)) + { + e("A role with the same name already exists."); + } + }); + } + + public static void CanDelete(Roles roles, DeleteRole command, AppContributors contributors, AppClients clients) + { + Guard.NotNull(command); + + CheckRoleExists(roles, command.Name); + + Validate.It(() => "Cannot delete role.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + else if (Roles.IsDefault(command.Name)) + { + e("Cannot delete a default role."); + } + + if (clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("Cannot remove a role when a client is assigned."); + } + + if (contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase))) + { + e("Cannot remove a role when a contributor is assigned."); + } + }); + } + + public static void CanUpdate(Roles roles, UpdateRole command) + { + Guard.NotNull(command); + + CheckRoleExists(roles, command.Name); + + Validate.It(() => "Cannot delete role.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + else if (Roles.IsDefault(command.Name)) + { + e("Cannot update a default role."); + } + + if (command.Permissions == null) + { + e(Not.Defined("Permissions"), nameof(command.Permissions)); + } + }); + } + + private static void CheckRoleExists(Roles roles, string name) + { + if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) + { + return; + } + + if (!roles.ContainsCustom(name)) + { + throw new DomainObjectNotFoundException(name, "Roles", typeof(IAppEntity)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs new file mode 100644 index 000000000..c0a6c90ee --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppWorkflows + { + public static void CanAdd(AddWorkflow command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add workflow.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + }); + } + + public static void CanUpdate(Workflows workflows, UpdateWorkflow command) + { + Guard.NotNull(command); + + CheckWorkflowExists(workflows, command.WorkflowId); + + Validate.It(() => "Cannot update workflow.", e => + { + if (command.Workflow == null) + { + e(Not.Defined("Workflow"), nameof(command.Workflow)); + return; + } + + var workflow = command.Workflow; + + if (!workflow.Steps.ContainsKey(workflow.Initial)) + { + e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); + } + + if (workflow.Initial == Status.Published) + { + e("Initial step cannot be published step.", $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); + } + + var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; + + if (!workflow.Steps.ContainsKey(Status.Published)) + { + e("Workflow must have a published step.", stepsPrefix); + } + + foreach (var step in workflow.Steps) + { + var stepPrefix = $"{stepsPrefix}.{step.Key}"; + + if (step.Value == null) + { + e(Not.Defined("Step"), stepPrefix); + } + else + { + foreach (var transition in step.Value.Transitions) + { + var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}"; + + if (!workflow.Steps.ContainsKey(transition.Key)) + { + e("Transition has an invalid target.", transitionPrefix); + } + + if (transition.Value == null) + { + e(Not.Defined("Transition"), transitionPrefix); + } + } + } + } + }); + } + + public static void CanDelete(Workflows workflows, DeleteWorkflow command) + { + Guard.NotNull(command); + + CheckWorkflowExists(workflows, command.WorkflowId); + } + + private static void CheckWorkflowExists(Workflows workflows, Guid id) + { + if (!workflows.ContainsKey(id)) + { + throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs new file mode 100644 index 000000000..3e288efba --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + string Name { get; } + + string? Label { get; } + + string? Description { get; } + + Roles Roles { get; } + + AppPlan? Plan { get; } + + AppImage? Image { get; } + + AppClients Clients { get; } + + AppPatterns Patterns { get; } + + AppContributors Contributors { get; } + + LanguagesConfig LanguagesConfig { get; } + + Workflows Workflows { get; } + + bool IsArchived { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs new file mode 100644 index 000000000..b6a46d78f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppUISettings + { + Task GetAsync(Guid appId, string? userId); + + Task SetAsync(Guid appId, string? userId, string path, IJsonValue value); + + Task SetAsync(Guid appId, string? userId, JsonObject settings); + + Task RemoveAsync(Guid appId, string? userId, string path); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs new file mode 100644 index 000000000..ce416a3c1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -0,0 +1,286 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsIndex : IAppsIndex, ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public AppsIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task RebuildByContributorsAsync(Guid appId, HashSet contributors) + { + foreach (var contributorId in contributors) + { + await Index(contributorId).AddAsync(appId); + } + } + + public Task RebuildByContributorsAsync(string contributorId, HashSet apps) + { + return Index(contributorId).RebuildAsync(apps); + } + + public Task RebuildAsync(Dictionary appsByName) + { + return Index().RebuildAsync(appsByName); + } + + public Task RemoveReservationAsync(string? token) + { + return Index().RemoveReservationAsync(token); + } + + public Task> GetIdsAsync() + { + return Index().GetIdsAsync(); + } + + public Task AddAsync(string? token) + { + return Index().AddAsync(token); + } + + public Task ReserveAsync(Guid id, string name) + { + return Index().ReserveAsync(id, name); + } + + public async Task> GetAppsAsync() + { + using (Profiler.TraceMethod()) + { + var ids = await GetAppIdsAsync(); + + var apps = + await Task.WhenAll(ids + .Select(id => GetAppAsync(id))); + + return apps.Where(x => x != null).ToList(); + } + } + + public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions) + { + using (Profiler.TraceMethod()) + { + var ids = + await Task.WhenAll( + GetAppIdsByUserAsync(userId), + GetAppIdsAsync(permissions.ToAppNames())); + + var apps = + await Task.WhenAll(ids + .SelectMany(x => x) + .Select(id => GetAppAsync(id))); + + return apps.Where(x => x != null).ToList(); + } + } + + public async Task GetAppByNameAsync(string name) + { + using (Profiler.TraceMethod()) + { + var appId = await GetAppIdAsync(name); + + if (appId == default) + { + return null; + } + + return await GetAppAsync(appId); + } + } + + public async Task GetAppAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + var app = await grainFactory.GetGrain(appId).GetStateAsync(); + + if (IsFound(app.Value)) + { + return app.Value; + } + + return null; + } + } + + private async Task> GetAppIdsByUserAsync(string userId) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(userId).GetIdsAsync(); + } + } + + private async Task> GetAppIdsAsync() + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(); + } + } + + private async Task> GetAppIdsAsync(string[] names) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(names); + } + } + + private async Task GetAppIdAsync(string name) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdAsync(name); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is CreateApp createApp) + { + var index = Index(); + + var token = await CheckAppAsync(index, createApp); + + try + { + await next(); + } + finally + { + if (token != null) + { + if (context.IsCompleted) + { + await index.AddAsync(token); + + if (createApp.Actor.IsSubject) + { + await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId); + } + } + else + { + await index.RemoveReservationAsync(token); + } + } + } + } + else + { + await next(); + + if (context.IsCompleted) + { + switch (context.Command) + { + case AssignContributor assignContributor: + await AssignContributorAsync(assignContributor); + break; + + case RemoveContributor removeContributor: + await RemoveContributorAsync(removeContributor); + break; + + case ArchiveApp archiveApp: + await ArchiveAppAsync(archiveApp); + break; + } + } + } + } + + private async Task CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command) + { + var name = command.Name; + + if (name.IsSlug()) + { + var token = await index.ReserveAsync(command.AppId, name); + + if (token == null) + { + var error = new ValidationError("An app with this already exists."); + + throw new ValidationException("Cannot create app.", error); + } + + return token; + } + + return null; + } + + private Task AssignContributorAsync(AssignContributor command) + { + return Index(command.ContributorId).AddAsync(command.AppId); + } + + private Task RemoveContributorAsync(RemoveContributor command) + { + return Index(command.ContributorId).RemoveAsync(command.AppId); + } + + private async Task ArchiveAppAsync(ArchiveApp command) + { + var appId = command.AppId; + + var app = await grainFactory.GetGrain(appId).GetStateAsync(); + + if (IsFound(app.Value)) + { + await Index().RemoveAsync(appId); + } + + foreach (var contributorId in app.Value.Contributors.Keys) + { + await Index(contributorId).RemoveAsync(appId); + } + } + + private static bool IsFound(IAppEntity app) + { + return app.Version > EtagVersion.Empty && !app.IsArchived; + } + + private IAppsByNameIndexGrain Index() + { + return grainFactory.GetGrain(SingleGrain.Id); + } + + private IAppsByUserIndexGrain Index(string id) + { + return grainFactory.GetGrain(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs new file mode 100644 index 000000000..383c349d3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public interface IAppsIndex + { + Task> GetIdsAsync(); + + Task> GetAppsAsync(); + + Task> GetAppsForUserAsync(string userId, PermissionSet permissions); + + Task GetAppByNameAsync(string name); + + Task GetAppAsync(Guid appId); + + Task ReserveAsync(Guid id, string name); + + Task AddAsync(string? token); + + Task RemoveReservationAsync(string? token); + + Task RebuildByContributorsAsync(string contributorId, HashSet apps); + + Task RebuildAsync(Dictionary apps); + + Task RebuildByContributorsAsync(Guid appId, HashSet contributors); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs new file mode 100644 index 000000000..a0362cb85 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitation +{ + public sealed class InviteUserCommandMiddleware : ICommandMiddleware + { + private readonly IUserResolver userResolver; + + public InviteUserCommandMiddleware(IUserResolver userResolver) + { + Guard.NotNull(userResolver); + + this.userResolver = userResolver; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is AssignContributor assignContributor && ShouldInvite(assignContributor)) + { + var created = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true); + + await next(); + + if (created && context.PlainResult is IAppEntity app) + { + context.Complete(new InvitedResult { App = app }); + } + } + else + { + await next(); + } + } + + private static bool ShouldInvite(AssignContributor assignContributor) + { + return assignContributor.Invite && assignContributor.ContributorId.IsEmail(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs new file mode 100644 index 000000000..c2c2e100f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +#pragma warning disable IDE0028 // Simplify collection initialization + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class RolePermissionsProvider + { + private readonly IAppProvider appProvider; + + public RolePermissionsProvider(IAppProvider appProvider) + { + Guard.NotNull(appProvider); + + this.appProvider = appProvider; + } + + public async Task> GetPermissionsAsync(IAppEntity app) + { + var schemaNames = await GetSchemaNamesAsync(app); + + var result = new List { Permission.Any }; + + foreach (var permission in Permissions.ForAppsNonSchema) + { + if (permission.Length > Permissions.App.Length + 1) + { + var trimmed = permission.Substring(Permissions.App.Length + 1); + + if (trimmed.Length > 0) + { + result.Add(trimmed); + } + } + } + + foreach (var permission in Permissions.ForAppsSchema) + { + var trimmed = permission.Substring(Permissions.App.Length + 1); + + foreach (var schema in schemaNames) + { + var replaced = trimmed.Replace("{name}", schema); + + result.Add(replaced); + } + } + + return result; + } + + private async Task> GetSchemaNamesAsync(IAppEntity app) + { + var schemas = await appProvider.GetSchemasAsync(app.Id); + + var schemaNames = new List(); + + schemaNames.Add(Permission.Any); + schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name)); + + return schemaNames; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs new file mode 100644 index 000000000..764150bbd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppLimitsPlan + { + string Id { get; } + + string Name { get; } + + string Costs { get; } + + string? YearlyCosts { get; } + + string? YearlyId { get; } + + long MaxApiCalls { get; } + + long MaxAssetSize { get; } + + int MaxContributors { get; } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs new file mode 100644 index 000000000..3a36705ba --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppPlanBillingManager + { + bool HasPortal { get; } + + Task ChangePlanAsync(string userId, NamedId appId, string? planId); + + Task GetPortalLinkAsync(string userId); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs new file mode 100644 index 000000000..c73aa5dc1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// 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.Entities.Apps.Services +{ + public interface IAppPlansProvider + { + IEnumerable GetAvailablePlans(); + + bool IsConfiguredPlan(string? planId); + + IAppLimitsPlan? GetPlanUpgradeForApp(IAppEntity app); + + IAppLimitsPlan? GetPlanUpgrade(string? planId); + + IAppLimitsPlan? GetPlan(string? planId); + + IAppLimitsPlan GetPlanForApp(IAppEntity app); + + IAppLimitsPlan GetFreePlan(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs new file mode 100644 index 000000000..1548f3ea6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class ConfigAppLimitsPlan : IAppLimitsPlan + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Costs { get; set; } + + public string? YearlyCosts { get; set; } + + public string? YearlyId { get; set; } + + public long MaxApiCalls { get; set; } + + public long MaxAssetSize { get; set; } + + public int MaxContributors { get; set; } + + public ConfigAppLimitsPlan Clone() + { + return (ConfigAppLimitsPlan)MemberwiseClone(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs new file mode 100644 index 000000000..3be58073c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// 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.Entities.Apps.Services.Implementations +{ + public sealed class ConfigAppPlansProvider : IAppPlansProvider + { + private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1 + }; + + private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly List plansList = new List(); + + public ConfigAppPlansProvider(IEnumerable config) + { + Guard.NotNull(config); + + foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone())) + { + plansList.Add(plan); + plansById[plan.Id] = plan; + + if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) + { + plansById[plan.YearlyId] = plan; + } + } + } + + public IEnumerable GetAvailablePlans() + { + return plansList; + } + + public bool IsConfiguredPlan(string? planId) + { + return planId != null && plansById.ContainsKey(planId); + } + + public IAppLimitsPlan? GetPlan(string? planId) + { + return plansById.GetOrDefault(planId ?? string.Empty); + } + + public IAppLimitsPlan GetPlanForApp(IAppEntity app) + { + Guard.NotNull(app); + + return GetPlanCore(app.Plan?.PlanId); + } + + public IAppLimitsPlan GetFreePlan() + { + return GetPlanCore(plansList.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.Costs))?.Id); + } + + public IAppLimitsPlan? GetPlanUpgradeForApp(IAppEntity app) + { + Guard.NotNull(app); + + return GetPlanUpgrade(app.Plan?.PlanId); + } + + public IAppLimitsPlan? GetPlanUpgrade(string? planId) + { + var plan = GetPlanCore(planId); + + var nextPlanIndex = plansList.IndexOf(plan); + + if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) + { + return plansList[nextPlanIndex + 1]; + } + + return null; + } + + private ConfigAppLimitsPlan GetPlanCore(string? planId) + { + return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs new file mode 100644 index 000000000..418aa4814 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager + { + public bool HasPortal + { + get { return false; } + } + + public Task ChangePlanAsync(string userId, NamedId appId, string? planId) + { + return Task.FromResult(new PlanResetResult()); + } + + public Task GetPortalLinkAsync(string userId) + { + return Task.FromResult(string.Empty); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanResetResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs new file mode 100644 index 000000000..956ffb1a9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// 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.Entities.Apps.Services +{ + public sealed class RedirectToCheckoutResult : IChangePlanResult + { + public Uri Url { get; } + + public RedirectToCheckoutResult(Uri url) + { + Guard.NotNull(url); + + Url = url; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs new file mode 100644 index 000000000..fb600d5b7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -0,0 +1,252 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.Serialization; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Apps.State +{ + [CollectionName("Apps")] + public class AppState : DomainObjectState, IAppEntity + { + [DataMember] + public string Name { get; set; } + + [DataMember] + public string Label { get; set; } + + [DataMember] + public string Description { get; set; } + + [DataMember] + public Roles Roles { get; set; } = Roles.Empty; + + [DataMember] + public AppPlan? Plan { get; set; } + + [DataMember] + public AppImage? Image { get; set; } + + [DataMember] + public AppClients Clients { get; set; } = AppClients.Empty; + + [DataMember] + public AppPatterns Patterns { get; set; } = AppPatterns.Empty; + + [DataMember] + public AppContributors Contributors { get; set; } = AppContributors.Empty; + + [DataMember] + public LanguagesConfig LanguagesConfig { get; set; } = LanguagesConfig.English; + + [DataMember] + public Workflows Workflows { get; set; } = Workflows.Empty; + + [DataMember] + public bool IsArchived { get; set; } + + public void ApplyEvent(IEvent @event) + { + switch (@event) + { + case AppCreated e: + { + SimpleMapper.Map(e, this); + + break; + } + + case AppUpdated e: + { + SimpleMapper.Map(e, this); + + break; + } + + case AppImageUploaded e: + { + Image = e.Image; + + break; + } + + case AppImageRemoved _: + { + Image = null; + + break; + } + + case AppPlanChanged e: + { + Plan = AppPlan.Build(e.Actor, e.PlanId); + + break; + } + + case AppPlanReset _: + { + Plan = null; + + break; + } + + case AppContributorAssigned e: + { + Contributors = Contributors.Assign(e.ContributorId, e.Role); + + break; + } + + case AppContributorRemoved e: + { + Contributors = Contributors.Remove(e.ContributorId); + + break; + } + + case AppClientAttached e: + { + Clients = Clients.Add(e.Id, e.Secret); + + break; + } + + case AppClientUpdated e: + { + Clients = Clients.Update(e.Id, e.Role); + + break; + } + + case AppClientRenamed e: + { + Clients = Clients.Rename(e.Id, e.Name); + + break; + } + + case AppClientRevoked e: + { + Clients = Clients.Revoke(e.Id); + + break; + } + + case AppWorkflowAdded e: + { + Workflows = Workflows.Add(e.WorkflowId, e.Name); + + break; + } + + case AppWorkflowUpdated e: + { + Workflows = Workflows.Update(e.WorkflowId, e.Workflow); + + break; + } + + case AppWorkflowDeleted e: + { + Workflows = Workflows.Remove(e.WorkflowId); + + break; + } + + case AppPatternAdded e: + { + Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message); + + break; + } + + case AppPatternDeleted e: + { + Patterns = Patterns.Remove(e.PatternId); + + break; + } + + case AppPatternUpdated e: + { + Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message); + + break; + } + + case AppRoleAdded e: + { + Roles = Roles.Add(e.Name); + + break; + } + + case AppRoleDeleted e: + { + Roles = Roles.Remove(e.Name); + + break; + } + + case AppRoleUpdated e: + { + Roles = Roles.Update(e.Name, e.Permissions); + + break; + } + + case AppLanguageAdded e: + { + LanguagesConfig = LanguagesConfig.Set(e.Language); + + break; + } + + case AppLanguageRemoved e: + { + LanguagesConfig = LanguagesConfig.Remove(e.Language); + + break; + } + + case AppLanguageUpdated e: + { + LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback); + + if (e.IsMaster) + { + LanguagesConfig = LanguagesConfig.MakeMaster(e.Language); + } + + break; + } + + case AppArchived _: + { + Plan = null; + + IsArchived = true; + + break; + } + } + } + + public override AppState Apply(Envelope @event) + { + return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs new file mode 100644 index 000000000..31fdbf82a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public abstract class FieldBuilder + { + private readonly UpsertSchemaField field; + + protected T Properties() where T : FieldProperties + { + return (T)field.Properties; + } + + protected FieldBuilder(UpsertSchemaField field) + { + this.field = field; + } + + public FieldBuilder Label(string? label) + { + field.Properties.Label = label; + + return this; + } + + public FieldBuilder Hints(string? hints) + { + field.Properties.Hints = hints; + + return this; + } + + public FieldBuilder Localizable() + { + field.Partitioning = Partitioning.Language.Key; + + return this; + } + + public FieldBuilder Disabled() + { + field.IsDisabled = true; + + return this; + } + + public FieldBuilder Required() + { + field.Properties.IsRequired = true; + + return this; + } + + public FieldBuilder ShowInList() + { + field.Properties.IsListField = true; + + return this; + } + + public FieldBuilder ShowInReferences() + { + field.Properties.IsReferenceField = true; + + return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs new file mode 100644 index 000000000..8ee0eea85 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public sealed class SchemaBuilder + { + private readonly CreateSchema command; + + public SchemaBuilder(CreateSchema command) + { + this.command = command; + } + + public static SchemaBuilder Create(string name) + { + var schemaName = name.ToKebabCase(); + + return new SchemaBuilder(new CreateSchema + { + Name = schemaName + }).Published().WithLabel(name); + } + + public SchemaBuilder WithLabel(string? label) + { + command.Properties ??= new SchemaProperties(); + command.Properties.Label = label; + + return this; + } + + public SchemaBuilder WithScripts(SchemaScripts scripts) + { + command.Scripts = scripts; + + return this; + } + + public SchemaBuilder Published() + { + command.IsPublished = true; + + return this; + } + + public SchemaBuilder Singleton() + { + command.IsSingleton = true; + + return this; + } + + public SchemaBuilder AddAssets(string name, Action configure) + { + var field = AddField(name); + + configure(new AssetFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddBoolean(string name, Action configure) + { + var field = AddField(name); + + configure(new BooleanFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddDateTime(string name, Action configure) + { + var field = AddField(name); + + configure(new DateTimeFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddJson(string name, Action configure) + { + var field = AddField(name); + + configure(new JsonFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddNumber(string name, Action configure) + { + var field = AddField(name); + + configure(new NumberFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddString(string name, Action configure) + { + var field = AddField(name); + + configure(new StringFieldBuilder(field)); + + return this; + } + + public SchemaBuilder AddTags(string name, Action configure) + { + var field = AddField(name); + + configure(new TagsFieldBuilder(field)); + + return this; + } + + private UpsertSchemaField AddField(string name) where T : FieldProperties, new() + { + var field = new UpsertSchemaField + { + Name = name.ToCamelCase(), + Properties = new T + { + Label = name + } + }; + + command.Fields ??= new List(); + command.Fields.Add(field); + + return field; + } + + public CreateSchema Build() + { + return command; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs new file mode 100644 index 000000000..75ce75746 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public class StringFieldBuilder : FieldBuilder + { + public StringFieldBuilder(UpsertSchemaField field) + : base(field) + { + } + + public StringFieldBuilder AsTextArea() + { + Properties().Editor = StringFieldEditor.TextArea; + + return this; + } + + public StringFieldBuilder AsRichText() + { + Properties().Editor = StringFieldEditor.RichText; + + return this; + } + + public StringFieldBuilder AsDropDown(params string[] values) + { + Properties().AllowedValues = ReadOnlyCollection.Create(values); + Properties().Editor = StringFieldEditor.Dropdown; + + return this; + } + + public StringFieldBuilder Pattern(string pattern, string? message = null) + { + Properties().Pattern = pattern; + Properties().PatternMessage = message; + + return this; + } + + public StringFieldBuilder Length(int maxLength, int minLength = 0) + { + Properties().MaxLength = maxLength; + Properties().MinLength = minLength; + + return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs new file mode 100644 index 000000000..7af666c55 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IAssetLoader assetLoader; + + public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IAssetLoader assetLoader) + { + Guard.NotNull(scriptEngine); + Guard.NotNull(assetLoader); + + this.scriptEngine = scriptEngine; + + this.assetLoader = assetLoader; + } + + protected override async Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedAssetEvent(); + + var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber()); + + SimpleMapper.Map(asset, result); + + switch (@event.Payload) + { + case AssetCreated _: + result.Type = EnrichedAssetEventType.Created; + break; + case AssetAnnotated _: + result.Type = EnrichedAssetEventType.Annotated; + break; + case AssetUpdated _: + result.Type = EnrichedAssetEventType.Updated; + break; + case AssetDeleted _: + result.Type = EnrichedAssetEventType.Deleted; + break; + } + + result.Name = $"Asset{result.Type}"; + + return result; + } + + protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) + { + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs new file mode 100644 index 000000000..0a91fd30a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -0,0 +1,175 @@ +// ========================================================================== +// 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.Security.Cryptography; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetCommandMiddleware : GrainCommandMiddleware + { + private readonly IAssetStore assetStore; + private readonly IAssetEnricher assetEnricher; + private readonly IAssetQueryService assetQuery; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IContextProvider contextProvider; + private readonly IEnumerable> tagGenerators; + + public AssetCommandMiddleware( + IGrainFactory grainFactory, + IAssetEnricher assetEnricher, + IAssetQueryService assetQuery, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IContextProvider contextProvider, + IEnumerable> tagGenerators) + : base(grainFactory) + { + Guard.NotNull(assetEnricher); + Guard.NotNull(assetStore); + Guard.NotNull(assetQuery); + Guard.NotNull(assetThumbnailGenerator); + Guard.NotNull(contextProvider); + Guard.NotNull(tagGenerators); + + this.assetStore = assetStore; + this.assetEnricher = assetEnricher; + this.assetQuery = assetQuery; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.contextProvider = contextProvider; + this.tagGenerators = tagGenerators; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + var tempFile = context.ContextId.ToString(); + + switch (context.Command) + { + case CreateAsset createAsset: + { + await EnrichWithImageInfosAsync(createAsset); + await EnrichWithHashAndUploadAsync(createAsset, tempFile); + + try + { + var ctx = contextProvider.Context.Clone().WithNoAssetEnrichment(); + + var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); + + foreach (var existing in existings) + { + if (IsDuplicate(existing, createAsset.File)) + { + var result = new AssetCreatedResult(existing, true); + + context.Complete(result); + + await next(); + return; + } + } + + GenerateTags(createAsset); + + await HandleCoreAsync(context, next); + + var asset = context.Result(); + + context.Complete(new AssetCreatedResult(asset, false)); + + await assetStore.CopyAsync(tempFile, createAsset.AssetId.ToString(), asset.FileVersion, null); + } + finally + { + await assetStore.DeleteAsync(tempFile); + } + + break; + } + + case UpdateAsset updateAsset: + { + await EnrichWithImageInfosAsync(updateAsset); + await EnrichWithHashAndUploadAsync(updateAsset, tempFile); + + try + { + await HandleCoreAsync(context, next); + + var asset = context.Result(); + + await assetStore.CopyAsync(tempFile, updateAsset.AssetId.ToString(), asset.FileVersion, null); + } + finally + { + await assetStore.DeleteAsync(tempFile); + } + + break; + } + + default: + await HandleCoreAsync(context, next); + break; + } + } + + private async Task HandleCoreAsync(CommandContext context, Func next) + { + await base.HandleAsync(context, next); + + if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity)) + { + var enriched = await assetEnricher.EnrichAsync(asset, contextProvider.Context); + + context.Complete(enriched); + } + } + + private static bool IsDuplicate(IAssetEntity asset, AssetFile file) + { + return asset?.FileName == file.FileName && asset.FileSize == file.FileSize; + } + + private async Task EnrichWithImageInfosAsync(UploadAssetCommand command) + { + command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); + } + + private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) + { + using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256)) + { + await assetStore.UploadAsync(tempFile, hashStream); + + command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); + } + } + + private void GenerateTags(CreateAsset createAsset) + { + if (createAsset.Tags == null) + { + createAsset.Tags = new HashSet(); + } + + foreach (var tagGenerator in tagGenerators) + { + tagGenerator.GenerateTags(createAsset, createAsset.Tags); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs new file mode 100644 index 000000000..7b31dbfcc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -0,0 +1,183 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Guards; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetGrain : LogSnapshotDomainObjectGrain, IAssetGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + private readonly ITagService tagService; + + public AssetGrain(IStore store, ITagService tagService, IActivationLimit limit, ISemanticLog log) + : base(store, log) + { + Guard.NotNull(tagService); + + this.tagService = tagService; + + limit?.SetLimit(5000, Lifetime); + } + + protected override Task OnActivateAsync(Guid key) + { + TryDelayDeactivation(Lifetime); + + return base.OnActivateAsync(key); + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateAsset createAsset: + return CreateReturnAsync(createAsset, async c => + { + GuardAsset.CanCreate(c); + + var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); + + Create(c, tagIds); + + return Snapshot; + }); + case UpdateAsset updateAsset: + return UpdateReturn(updateAsset, c => + { + GuardAsset.CanUpdate(c); + + Update(c); + + return Snapshot; + }); + case AnnotateAsset annotateAsset: + return UpdateReturnAsync(annotateAsset, async c => + { + GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); + + var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); + + Annotate(c, tagIds); + + return Snapshot; + }); + case DeleteAsset deleteAsset: + return UpdateAsync(deleteAsset, async c => + { + GuardAsset.CanDelete(c); + + await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags); + + Delete(c); + }); + default: + throw new NotSupportedException(); + } + } + + private async Task?> NormalizeTagsAsync(Guid appId, HashSet tags) + { + if (tags == null) + { + return null; + } + + var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); + + return new HashSet(normalized.Values); + } + + public void Create(CreateAsset command, HashSet? tagIds) + { + var @event = SimpleMapper.Map(command, new AssetCreated + { + IsImage = command.ImageInfo != null, + FileName = command.File.FileName, + FileSize = command.File.FileSize, + FileVersion = 0, + MimeType = command.File.MimeType, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight, + Slug = command.File.FileName.ToAssetSlug() + }); + + @event.Tags = tagIds; + + RaiseEvent(@event); + } + + public void Update(UpdateAsset command) + { + var @event = SimpleMapper.Map(command, new AssetUpdated + { + FileVersion = Snapshot.FileVersion + 1, + FileSize = command.File.FileSize, + MimeType = command.File.MimeType, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight, + IsImage = command.ImageInfo != null + }); + + RaiseEvent(@event); + } + + public void Annotate(AnnotateAsset command, HashSet? tagIds) + { + var @event = SimpleMapper.Map(command, new AssetAnnotated()); + + @event.Tags = tagIds; + + RaiseEvent(@event); + } + + public void Delete(DeleteAsset command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Asset has already been deleted"); + } + } + + public Task> GetStateAsync(long version = EtagVersion.Any) + { + return J.AsTask(GetSnapshot(version)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs new file mode 100644 index 000000000..c34fc9dc9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.UsageTracking; + +#pragma warning disable CS0649 + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public partial class AssetUsageTracker : IAssetUsageTracker, IEventConsumer + { + private const string Category = "Default"; + private const string CounterTotalCount = "TotalAssets"; + private const string CounterTotalSize = "TotalSize"; + private static readonly DateTime SummaryDate; + private readonly IUsageRepository usageStore; + + public AssetUsageTracker(IUsageRepository usageStore) + { + Guard.NotNull(usageStore); + + this.usageStore = usageStore; + } + + public async Task GetTotalSizeAsync(Guid appId) + { + var key = GetKey(appId); + + var entries = await usageStore.QueryAsync(key, SummaryDate, SummaryDate); + + return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault(); + } + + public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) + { + var enriched = new List(); + + var usagesFlat = await usageStore.QueryAsync(GetKey(appId), fromDate, toDate); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var stored = usagesFlat.FirstOrDefault(x => x.Date == date && x.Category == Category); + + var totalCount = 0L; + var totalSize = 0L; + + if (stored != null) + { + totalCount = (long)stored.Counters.Get(CounterTotalCount); + totalSize = (long)stored.Counters.Get(CounterTotalSize); + } + + enriched.Add(new AssetStats(date, totalCount, totalSize)); + } + + return enriched; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs new file mode 100644 index 000000000..aa572fa9c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -0,0 +1,127 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class BackupAssets : BackupHandlerWithStore + { + private const string TagsFile = "AssetTags.json"; + private readonly HashSet assetIds = new HashSet(); + private readonly IAssetStore assetStore; + private readonly ITagService tagService; + + public override string Name { get; } = "Assets"; + + public BackupAssets(IStore store, IAssetStore assetStore, ITagService tagService) + : base(store) + { + Guard.NotNull(assetStore); + Guard.NotNull(tagService); + + this.assetStore = assetStore; + + this.tagService = tagService; + } + + public override Task BackupAsync(Guid appId, BackupWriter writer) + { + return BackupTagsAsync(appId, writer); + } + + public override Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) + { + switch (@event.Payload) + { + case AssetCreated assetCreated: + return WriteAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, writer); + case AssetUpdated assetUpdated: + return WriteAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, writer); + } + + return TaskHelper.Done; + } + + public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case AssetCreated assetCreated: + await ReadAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, reader); + break; + case AssetUpdated assetUpdated: + await ReadAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, reader); + break; + } + + return true; + } + + public override async Task RestoreAsync(Guid appId, BackupReader reader) + { + await RestoreTagsAsync(appId, reader); + + await RebuildManyAsync(assetIds, RebuildAsync); + } + + private async Task RestoreTagsAsync(Guid appId, BackupReader reader) + { + var tags = await reader.ReadJsonAttachmentAsync(TagsFile); + + await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); + } + + private async Task BackupTagsAsync(Guid appId, BackupWriter writer) + { + var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); + + await writer.WriteJsonAsync(TagsFile, tags); + } + + private Task WriteAssetAsync(Guid assetId, long fileVersion, BackupWriter writer) + { + return writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => + { + return assetStore.DownloadAsync(assetId.ToString(), fileVersion, null, stream); + }); + } + + private Task ReadAssetAsync(Guid assetId, long fileVersion, BackupReader reader) + { + assetIds.Add(assetId); + + return reader.ReadBlobAsync(GetName(reader.OldGuid(assetId), fileVersion), async stream => + { + try + { + await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream, true); + } + catch (AssetAlreadyExistsException) + { + return; + } + }); + } + + private static string GetName(Guid assetId, long fileVersion) + { + return $"{assetId}_{fileVersion}.asset"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs new file mode 100644 index 000000000..6a519931c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public abstract class UploadAssetCommand : AssetCommand + { + public AssetFile File { get; set; } + + public ImageInfo? ImageInfo { get; set; } + + public string FileHash { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs new file mode 100644 index 000000000..486083010 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public static class GuardAsset + { + public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot rename asset.", e => + { + if (string.IsNullOrWhiteSpace(command.FileName) && + string.IsNullOrWhiteSpace(command.Slug) && + command.Tags == null) + { + e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags)); + } + + if (!string.IsNullOrWhiteSpace(command.FileName) && string.Equals(command.FileName, oldFileName)) + { + e(Not.New("Asset", "name"), nameof(command.FileName)); + } + + if (!string.IsNullOrWhiteSpace(command.Slug) && string.Equals(command.Slug, oldSlug)) + { + e(Not.New("Asset", "slug"), nameof(command.Slug)); + } + }); + } + + public static void CanCreate(CreateAsset command) + { + Guard.NotNull(command); + } + + public static void CanUpdate(UpdateAsset command) + { + Guard.NotNull(command); + } + + public static void CanDelete(DeleteAsset command) + { + Guard.NotNull(command); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs new file mode 100644 index 000000000..dc94e0293 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetQueryService + { + Task> QueryByHashAsync(Context context, Guid appId, string hash); + + Task> QueryAsync(Context context, Q query); + + Task FindAssetAsync(Context context, Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs new file mode 100644 index 000000000..78cdafee7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetEnricher : IAssetEnricher + { + private readonly ITagService tagService; + + public AssetEnricher(ITagService tagService) + { + Guard.NotNull(tagService); + + this.tagService = tagService; + } + + public async Task EnrichAsync(IAssetEntity asset, Context context) + { + Guard.NotNull(asset); + Guard.NotNull(context); + + var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1), context); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable assets, Context context) + { + Guard.NotNull(assets); + Guard.NotNull(context); + + using (Profiler.TraceMethod()) + { + var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); + + if (ShouldEnrich(context)) + { + await EnrichTagsAsync(results); + } + + return results; + } + } + + private async Task EnrichTagsAsync(List assets) + { + foreach (var group in assets.GroupBy(x => x.AppId.Id)) + { + var tagsById = await CalculateTags(group); + + foreach (var asset in group) + { + asset.TagNames = new HashSet(); + + if (asset.Tags != null) + { + foreach (var id in asset.Tags) + { + if (tagsById.TryGetValue(id, out var name)) + { + asset.TagNames.Add(name); + } + } + } + } + } + } + + private async Task> CalculateTags(IGrouping group) + { + var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); + + return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); + } + + private static bool ShouldEnrich(Context context) + { + return !context.IsNoAssetEnrichment(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs new file mode 100644 index 000000000..5265ae1fc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetLoader : IAssetLoader + { + private readonly IGrainFactory grainFactory; + + public AssetLoader(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid id, long version) + { + using (Profiler.TraceMethod()) + { + var grain = grainFactory.GetGrain(id); + + var content = await grain.GetStateAsync(version); + + if (content.Value == null || content.Value.Version != version) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IAssetEntity)); + } + + return content.Value; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs new file mode 100644 index 000000000..f91b8dd8d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -0,0 +1,174 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Microsoft.OData; +using Microsoft.OData.Edm; +using NJsonSchema; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Queries.OData; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class AssetQueryParser + { + private readonly JsonSchema jsonSchema = BuildJsonSchema(); + private readonly IEdmModel edmModel = BuildEdmModel(); + private readonly IJsonSerializer jsonSerializer; + private readonly ITagService tagService; + private readonly AssetOptions options; + + public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions options) + { + Guard.NotNull(jsonSerializer); + Guard.NotNull(options); + Guard.NotNull(tagService); + + this.jsonSerializer = jsonSerializer; + this.options = options.Value; + this.tagService = tagService; + } + + public virtual ClrQuery ParseQuery(Context context, Q q) + { + Guard.NotNull(context); + + using (Profiler.TraceMethod()) + { + var result = new ClrQuery(); + + if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) + { + result = ParseJson(q.JsonQuery); + } + else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + { + result = ParseOData(q.ODataQuery); + } + + if (result.Filter != null) + { + result.Filter = FilterTagTransformer.Transform(result.Filter, context.App.Id, tagService); + } + + if (result.Sort.Count == 0) + { + result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); + } + + if (result.Take == long.MaxValue) + { + result.Take = options.DefaultPageSize; + } + else if (result.Take > options.MaxResults) + { + result.Take = options.MaxResults; + } + + return result; + } + } + + private ClrQuery ParseJson(string json) + { + return jsonSchema.Parse(json, jsonSerializer); + } + + private ClrQuery ParseOData(string odata) + { + try + { + return edmModel.ParseQuery(odata).ToQuery(); + } + catch (NotSupportedException) + { + throw new ValidationException("OData operation is not supported."); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + + private static JsonSchema BuildJsonSchema() + { + var schema = new JsonSchema { Title = "Asset", Type = JsonObjectType.Object }; + + void AddProperty(string name, JsonObjectType type, string? format = null) + { + var property = new JsonSchemaProperty { Type = type, Format = format }; + + schema.Properties[name.ToCamelCase()] = property; + } + + AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid); + AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime); + AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime); + AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean); + AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer); + AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); + AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); + + return schema; + } + + private static IEdmModel BuildEdmModel() + { + var entityType = new EdmEntityType("Squidex", "Asset"); + + void AddProperty(string name, EdmPrimitiveTypeKind type) + { + entityType.AddStructuralProperty(name.ToCamelCase(), type); + } + + AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); + AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); + AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64); + AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); + AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); + AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); + AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); + AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); + AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String); + AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("AssetSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + return model; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs new file mode 100644 index 000000000..098b5a516 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetQueryService : IAssetQueryService + { + private readonly IAssetEnricher assetEnricher; + private readonly IAssetRepository assetRepository; + private readonly AssetQueryParser queryParser; + + public AssetQueryService( + IAssetEnricher assetEnricher, + IAssetRepository assetRepository, + AssetQueryParser queryParser) + { + Guard.NotNull(assetEnricher); + Guard.NotNull(assetRepository); + Guard.NotNull(queryParser); + + this.assetEnricher = assetEnricher; + this.assetRepository = assetRepository; + this.queryParser = queryParser; + } + + public async Task FindAssetAsync(Context context, Guid id) + { + var asset = await assetRepository.FindAssetAsync(id); + + if (asset != null) + { + return await assetEnricher.EnrichAsync(asset, context); + } + + return null; + } + + public async Task> QueryByHashAsync(Context context, Guid appId, string hash) + { + Guard.NotNull(hash); + + var assets = await assetRepository.QueryByHashAsync(appId, hash); + + return await assetEnricher.EnrichAsync(assets, context); + } + + public async Task> QueryAsync(Context context, Q query) + { + Guard.NotNull(context); + Guard.NotNull(query); + + IResultList assets; + + if (query.Ids != null && query.Ids.Count > 0) + { + assets = await QueryByIdsAsync(context, query); + } + else + { + assets = await QueryByQueryAsync(context, query); + } + + var enriched = await assetEnricher.EnrichAsync(assets, context); + + return ResultList.Create(assets.Total, enriched); + } + + private async Task> QueryByQueryAsync(Context context, Q query) + { + var parsedQuery = queryParser.ParseQuery(context, query); + + return await assetRepository.QueryAsync(context.App.Id, parsedQuery); + } + + private async Task> QueryByIdsAsync(Context context, Q query) + { + var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); + + return Sort(assets, query.Ids); + } + + private static IResultList Sort(IResultList assets, IReadOnlyList ids) + { + return assets.SortSet(x => x.Id, ids); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs new file mode 100644 index 000000000..d39af6a13 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class FilterTagTransformer : TransformVisitor + { + private readonly ITagService tagService; + private readonly Guid appId; + + private FilterTagTransformer(Guid appId, ITagService tagService) + { + this.appId = appId; + + this.tagService = tagService; + } + + public static FilterNode? Transform(FilterNode nodeIn, Guid appId, ITagService tagService) + { + Guard.NotNull(nodeIn); + Guard.NotNull(tagService); + + return nodeIn.Accept(new FilterTagTransformer(appId, tagService)); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) + { + var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; + + if (tagNames.TryGetValue(stringValue, out var normalized)) + { + return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); + } + } + + return nodeIn; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs new file mode 100644 index 000000000..11a45e228 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Assets.Repositories +{ + public interface IAssetRepository + { + Task> QueryByHashAsync(Guid appId, string hash); + + Task> QueryAsync(Guid appId, ClrQuery query); + + Task> QueryAsync(Guid appId, HashSet ids); + + Task FindAssetAsync(Guid id, bool allowDeleted = false); + + Task FindAssetBySlugAsync(Guid appId, string slug); + + Task RemoveAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs new file mode 100644 index 000000000..074b964a7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -0,0 +1,262 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Orleans.Concurrency; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + [Reentrant] + public sealed class BackupGrain : GrainOfGuid, IBackupGrain + { + private const int MaxBackups = 10; + private static readonly Duration UpdateDuration = Duration.FromSeconds(1); + private readonly IAssetStore assetStore; + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IClock clock; + private readonly IJsonSerializer serializer; + private readonly IServiceProvider serviceProvider; + private readonly IEventDataFormatter eventDataFormatter; + private readonly IEventStore eventStore; + private readonly ISemanticLog log; + private readonly IGrainState state; + private CancellationTokenSource? currentTask; + private BackupStateJob? currentJob; + + public BackupGrain( + IAssetStore assetStore, + IBackupArchiveLocation backupArchiveLocation, + IClock clock, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + IJsonSerializer serializer, + IServiceProvider serviceProvider, + ISemanticLog log, + IGrainState state) + { + Guard.NotNull(assetStore); + Guard.NotNull(backupArchiveLocation); + Guard.NotNull(clock); + Guard.NotNull(eventStore); + Guard.NotNull(eventDataFormatter); + Guard.NotNull(serviceProvider); + Guard.NotNull(serializer); + Guard.NotNull(state); + Guard.NotNull(log); + + this.assetStore = assetStore; + this.backupArchiveLocation = backupArchiveLocation; + this.clock = clock; + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.serializer = serializer; + this.serviceProvider = serviceProvider; + this.state = state; + this.log = log; + } + + protected override Task OnActivateAsync(Guid key) + { + RecoverAfterRestartAsync().Forget(); + + return TaskHelper.Done; + } + + private async Task RecoverAfterRestartAsync() + { + foreach (var job in state.Value.Jobs) + { + if (!job.Stopped.HasValue) + { + var jobId = job.Id.ToString(); + + job.Stopped = clock.GetCurrentInstant(); + + await Safe.DeleteAsync(backupArchiveLocation, jobId, log); + await Safe.DeleteAsync(assetStore, jobId, log); + + job.Status = JobStatus.Failed; + + await state.WriteAsync(); + } + } + } + + public async Task RunAsync() + { + if (currentTask != null) + { + throw new DomainException("Another backup process is already running."); + } + + if (state.Value.Jobs.Count >= MaxBackups) + { + throw new DomainException($"You cannot have more than {MaxBackups} backups."); + } + + var job = new BackupStateJob + { + Id = Guid.NewGuid(), + Started = clock.GetCurrentInstant(), + Status = JobStatus.Started + }; + + currentTask = new CancellationTokenSource(); + currentJob = job; + + state.Value.Jobs.Insert(0, job); + + await state.WriteAsync(); + + Process(job, currentTask.Token); + } + + private void Process(BackupStateJob job, CancellationToken ct) + { + ProcessAsync(job, ct).Forget(); + } + + private async Task ProcessAsync(BackupStateJob job, CancellationToken ct) + { + var jobId = job.Id.ToString(); + + var handlers = CreateHandlers(); + + var lastTimestamp = job.Started; + + try + { + using (var stream = await backupArchiveLocation.OpenStreamAsync(jobId)) + { + using (var writer = new BackupWriter(serializer, stream, true)) + { + await eventStore.QueryAsync(async storedEvent => + { + var @event = eventDataFormatter.Parse(storedEvent.Data); + + writer.WriteEvent(storedEvent); + + foreach (var handler in handlers) + { + await handler.BackupEventAsync(@event, Key, writer); + } + + job.HandledEvents = writer.WrittenEvents; + job.HandledAssets = writer.WrittenAttachments; + + lastTimestamp = await WritePeriodically(lastTimestamp); + }, SquidexHeaders.AppId, Key.ToString(), null, ct); + + foreach (var handler in handlers) + { + await handler.BackupAsync(Key, writer); + } + + foreach (var handler in handlers) + { + await handler.CompleteBackupAsync(Key, writer); + } + } + + stream.Position = 0; + + ct.ThrowIfCancellationRequested(); + + await assetStore.UploadAsync(jobId, 0, null, stream, false, ct); + } + + job.Status = JobStatus.Completed; + } + catch (Exception ex) + { + log.LogError(ex, jobId, (ctx, w) => w + .WriteProperty("action", "makeBackup") + .WriteProperty("status", "failed") + .WriteProperty("backupId", ctx)); + + job.Status = JobStatus.Failed; + } + finally + { + await Safe.DeleteAsync(backupArchiveLocation, jobId, log); + + job.Stopped = clock.GetCurrentInstant(); + + await state.WriteAsync(); + + currentTask = null; + currentJob = null; + } + } + + private async Task WritePeriodically(Instant lastTimestamp) + { + var now = clock.GetCurrentInstant(); + + if ((now - lastTimestamp) >= UpdateDuration) + { + lastTimestamp = now; + + await state.WriteAsync(); + } + + return lastTimestamp; + } + + public async Task DeleteAsync(Guid id) + { + var job = state.Value.Jobs.FirstOrDefault(x => x.Id == id); + + if (job == null) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IBackupJob)); + } + + if (currentJob == job) + { + currentTask?.Cancel(); + } + else + { + var jobId = job.Id.ToString(); + + await Safe.DeleteAsync(backupArchiveLocation, jobId, log); + await Safe.DeleteAsync(assetStore, jobId, log); + + state.Value.Jobs.Remove(job); + + await state.WriteAsync(); + } + } + + private IEnumerable CreateHandlers() + { + return serviceProvider.GetRequiredService>(); + } + + public Task>> GetStateAsync() + { + return J.AsTask(state.Value.Jobs.OfType().ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandler.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs new file mode 100644 index 000000000..e64b767fe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public abstract class BackupHandlerWithStore : BackupHandler + { + private readonly IStore store; + + protected BackupHandlerWithStore(IStore store) + { + Guard.NotNull(store); + + this.store = store; + } + + protected async Task RebuildManyAsync(IEnumerable ids, Func action) + { + foreach (var id in ids) + { + await action(id); + } + } + + protected async Task RebuildAsync(Guid key) where TState : IDomainState, new() + { + var state = new TState + { + Version = EtagVersion.Empty + }; + + var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => + { + state = state.Apply(e); + + state.Version++; + }); + + await persistence.ReadAsync(); + await persistence.WriteSnapshotAsync(state); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs new file mode 100644 index 000000000..da249a082 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -0,0 +1,155 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.Model; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.States; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class BackupReader : DisposableObjectBase + { + private readonly GuidMapper guidMapper = new GuidMapper(); + private readonly ZipArchive archive; + private readonly IJsonSerializer serializer; + private int readEvents; + private int readAttachments; + + public int ReadEvents + { + get { return readEvents; } + } + + public int ReadAttachments + { + get { return readAttachments; } + } + + public BackupReader(IJsonSerializer serializer, Stream stream) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + + archive = new ZipArchive(stream, ZipArchiveMode.Read, false); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + archive.Dispose(); + } + } + + public Guid OldGuid(Guid newId) + { + return guidMapper.OldGuid(newId); + } + + public Task ReadJsonAttachmentAsync(string name) + { + Guard.NotNullOrEmpty(name); + + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null) + { + throw new FileNotFoundException("Cannot find attachment.", name); + } + + T result; + + using (var stream = attachmentEntry.Open()) + { + result = serializer.Deserialize(stream, null, guidMapper.NewGuidOrValue); + } + + readAttachments++; + + return Task.FromResult(result); + } + + public async Task ReadBlobAsync(string name, Func handler) + { + Guard.NotNullOrEmpty(name); + Guard.NotNull(handler); + + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null) + { + throw new FileNotFoundException("Cannot find attachment.", name); + } + + using (var stream = attachmentEntry.Open()) + { + await handler(stream); + } + + readAttachments++; + } + + public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler) + { + Guard.NotNull(handler); + Guard.NotNull(formatter); + Guard.NotNull(streamNameResolver); + + while (true) + { + var eventEntry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); + + if (eventEntry == null) + { + break; + } + + using (var stream = eventEntry.Open()) + { + var (streamName, data) = serializer.Deserialize(stream).ToEvent(); + + MapHeaders(data); + + var eventStream = streamNameResolver.WithNewId(streamName, guidMapper.NewGuidOrNull); + var eventEnvelope = formatter.Parse(data, guidMapper.NewGuidOrValue); + + await handler((eventStream, eventEnvelope)); + } + + readEvents++; + } + } + + private void MapHeaders(EventData data) + { + foreach (var kvp in data.Headers.ToList()) + { + if (kvp.Value.Type == JsonValueType.String) + { + var newGuid = guidMapper.NewGuidOrNull(kvp.Value.ToString()); + + if (newGuid != null) + { + data.Headers.Add(kvp.Key, newGuid); + } + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs new file mode 100644 index 000000000..217f88541 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.Model; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class BackupWriter : DisposableObjectBase + { + private readonly ZipArchive archive; + private readonly IJsonSerializer serializer; + private readonly Func converter; + private int writtenEvents; + private int writtenAttachments; + + public int WrittenEvents + { + get { return writtenEvents; } + } + + public int WrittenAttachments + { + get { return writtenAttachments; } + } + + public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + + converter = + version == BackupVersion.V1 ? + new Func(CompatibleStoredEvent.V1) : + new Func(CompatibleStoredEvent.V2); + + archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + archive.Dispose(); + } + } + + public Task WriteJsonAsync(string name, object value) + { + Guard.NotNullOrEmpty(name); + + var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + + using (var stream = attachmentEntry.Open()) + { + serializer.Serialize(value, stream); + } + + writtenAttachments++; + + return TaskHelper.Done; + } + + public async Task WriteBlobAsync(string name, Func handler) + { + Guard.NotNullOrEmpty(name); + Guard.NotNull(handler); + + var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + + using (var stream = attachmentEntry.Open()) + { + await handler(stream); + } + + writtenAttachments++; + } + + public void WriteEvent(StoredEvent storedEvent) + { + Guard.NotNull(storedEvent); + + var eventEntry = archive.CreateEntry(ArchiveHelper.GetEventPath(writtenEvents)); + + using (var stream = eventEntry.Open()) + { + var @event = converter(storedEvent); + + serializer.Serialize(@event, stream); + } + + writtenEvents++; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs new file mode 100644 index 000000000..c1aed7a3f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// 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; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + internal sealed class GuidMapper + { + private static readonly int GuidLength = Guid.Empty.ToString().Length; + private readonly Dictionary oldToNewGuid = new Dictionary(); + private readonly Dictionary newToOldGuid = new Dictionary(); + private readonly Dictionary strings = new Dictionary(); + + public Guid OldGuid(Guid newGuid) + { + return newToOldGuid.GetOrCreate(newGuid, x => x); + } + + public string? NewGuidOrNull(string value) + { + if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) + { + return result; + } + + return null; + } + + public string NewGuidOrValue(string value) + { + if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) + { + return result; + } + + return value; + } + + private bool TryGenerateNewGuidString(string value, [MaybeNullWhen(false)] out string result) + { + if (value.Length == GuidLength) + { + if (strings.TryGetValue(value, out result!)) + { + return true; + } + + if (Guid.TryParse(value, out var guid)) + { + var newGuid = GenerateNewGuid(guid); + + strings[value] = result = newGuid.ToString(); + + return true; + } + } + + result = null!; + + return false; + } + + private bool TryGenerateNewNamedId(string value, [MaybeNullWhen(false)] out string result) + { + if (value.Length > GuidLength) + { + if (strings.TryGetValue(value, out result!)) + { + return true; + } + + if (NamedId.TryParse(value, Guid.TryParse, out var namedId)) + { + var newGuid = GenerateNewGuid(namedId.Id); + + strings[value] = result = NamedId.Of(newGuid, namedId.Name).ToString(); + + return true; + } + } + + result = null!; + + return false; + } + + private Guid GenerateNewGuid(Guid oldGuid) + { + return oldToNewGuid.GetOrAdd(oldGuid, GuidGenerator); + } + + private Guid GuidGenerator(Guid oldGuid) + { + var newGuid = Guid.NewGuid(); + + newToOldGuid[newGuid] = oldGuid; + + return newGuid; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs new file mode 100644 index 000000000..1cef6ad2a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Entities.Backup.Helpers +{ + public static class Downloader + { + public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, string id) + { + if (string.Equals(url.Scheme, "file")) + { + try + { + using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) + { + using (var sourceStream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read)) + { + await sourceStream.CopyToAsync(targetStream); + } + } + } + catch (IOException ex) + { + throw new BackupRestoreException($"Cannot download the archive: {ex.Message}.", ex); + } + } + else + { + HttpResponseMessage? response = null; + try + { + using (var client = new HttpClient()) + { + response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using (var sourceStream = await response.Content.ReadAsStreamAsync()) + { + using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) + { + await sourceStream.CopyToAsync(targetStream); + } + } + } + } + catch (HttpRequestException ex) + { + throw new BackupRestoreException($"Cannot download the archive. Got status code: {response?.StatusCode}.", ex); + } + } + } + + public static async Task OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, string id, IJsonSerializer serializer) + { + Stream? stream = null; + + try + { + stream = await backupArchiveLocation.OpenStreamAsync(id); + + return new BackupReader(serializer, stream); + } + catch (IOException) + { + stream?.Dispose(); + + throw new BackupRestoreException("The backup archive is correupt and cannot be opened."); + } + catch (Exception) + { + stream?.Dispose(); + + throw; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Safe.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs new file mode 100644 index 000000000..ada75140f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public interface IRestoreGrain : IGrainWithStringKey + { + Task RestoreAsync(Uri url, RefToken actor, string? newAppName = null); + + Task> GetJobAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs new file mode 100644 index 000000000..38ebeb0a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -0,0 +1,367 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Backup.Helpers; +using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class RestoreGrain : GrainOfString, IRestoreGrain + { + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IClock clock; + private readonly ICommandBus commandBus; + private readonly IJsonSerializer serializer; + private readonly IEventStore eventStore; + private readonly IEventDataFormatter eventDataFormatter; + private readonly ISemanticLog log; + private readonly IServiceProvider serviceProvider; + private readonly IStreamNameResolver streamNameResolver; + private readonly IGrainState state; + + private RestoreStateJob CurrentJob + { + get { return state.Value.Job; } + } + + public RestoreGrain(IBackupArchiveLocation backupArchiveLocation, + IClock clock, + ICommandBus commandBus, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + IJsonSerializer serializer, + ISemanticLog log, + IServiceProvider serviceProvider, + IStreamNameResolver streamNameResolver, + IGrainState state) + { + Guard.NotNull(backupArchiveLocation); + Guard.NotNull(clock); + Guard.NotNull(commandBus); + Guard.NotNull(eventStore); + Guard.NotNull(eventDataFormatter); + Guard.NotNull(serializer); + Guard.NotNull(serviceProvider); + Guard.NotNull(state); + Guard.NotNull(streamNameResolver); + Guard.NotNull(log); + + this.backupArchiveLocation = backupArchiveLocation; + this.clock = clock; + this.commandBus = commandBus; + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.serializer = serializer; + this.serviceProvider = serviceProvider; + this.streamNameResolver = streamNameResolver; + this.state = state; + this.log = log; + } + + protected override Task OnActivateAsync(string key) + { + RecoverAfterRestartAsync().Forget(); + + return TaskHelper.Done; + } + + private async Task RecoverAfterRestartAsync() + { + if (CurrentJob?.Status == JobStatus.Started) + { + var handlers = CreateHandlers(); + + Log("Failed due application restart"); + + CurrentJob.Status = JobStatus.Failed; + + await CleanupAsync(handlers); + + await state.WriteAsync(); + } + } + + public async Task RestoreAsync(Uri url, RefToken actor, string? newAppName) + { + Guard.NotNull(url); + Guard.NotNull(actor); + + if (!string.IsNullOrWhiteSpace(newAppName)) + { + Guard.ValidSlug(newAppName); + } + + if (CurrentJob?.Status == JobStatus.Started) + { + throw new DomainException("A restore operation is already running."); + } + + state.Value.Job = new RestoreStateJob + { + Id = Guid.NewGuid(), + NewAppName = newAppName, + Actor = actor, + Started = clock.GetCurrentInstant(), + Status = JobStatus.Started, + Url = url + }; + + await state.WriteAsync(); + + Process(); + } + + private void Process() + { + ProcessAsync().Forget(); + } + + private async Task ProcessAsync() + { + var handlers = CreateHandlers(); + + var logContext = (jobId: CurrentJob.Id.ToString(), jobUrl: CurrentJob.Url.ToString()); + + using (Profiler.StartSession()) + { + try + { + Log("Started. The restore process has the following steps:"); + Log(" * Download backup"); + Log(" * Restore events and attachments."); + Log(" * Restore all objects like app, schemas and contents"); + Log(" * Complete the restore operation for all objects"); + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "restore") + .WriteProperty("status", "started") + .WriteProperty("operationId", ctx.jobId) + .WriteProperty("url", ctx.jobUrl)); + + using (Profiler.Trace("Download")) + { + await DownloadAsync(); + } + + using (var reader = await backupArchiveLocation.OpenArchiveAsync(CurrentJob.Id.ToString(), serializer)) + { + using (Profiler.Trace("ReadEvents")) + { + await ReadEventsAsync(reader, handlers); + } + + foreach (var handler in handlers) + { + using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.RestoreAsync))) + { + await handler.RestoreAsync(CurrentJob.AppId, reader); + } + + Log($"Restored {handler.Name}"); + } + + foreach (var handler in handlers) + { + using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.CompleteRestoreAsync))) + { + await handler.CompleteRestoreAsync(CurrentJob.AppId, reader); + } + + Log($"Completed {handler.Name}"); + } + } + + await AssignContributorAsync(); + + CurrentJob.Status = JobStatus.Completed; + + Log("Completed, Yeah!"); + + log.LogInformation(logContext, (ctx, w) => + { + w.WriteProperty("action", "restore"); + w.WriteProperty("status", "completed"); + w.WriteProperty("operationId", ctx.jobId); + w.WriteProperty("url", ctx.jobUrl); + + Profiler.Session?.Write(w); + }); + } + catch (Exception ex) + { + if (ex is BackupRestoreException backupException) + { + Log(backupException.Message); + } + else + { + Log("Failed with internal error"); + } + + await CleanupAsync(handlers); + + CurrentJob.Status = JobStatus.Failed; + + log.LogError(ex, logContext, (ctx, w) => + { + w.WriteProperty("action", "retore"); + w.WriteProperty("status", "failed"); + w.WriteProperty("operationId", ctx.jobId); + w.WriteProperty("url", ctx.jobUrl); + + Profiler.Session?.Write(w); + }); + } + finally + { + CurrentJob.Stopped = clock.GetCurrentInstant(); + + await state.WriteAsync(); + } + } + } + + private async Task AssignContributorAsync() + { + var actor = CurrentJob.Actor; + + if (actor?.IsSubject == true) + { + try + { + await commandBus.PublishAsync(new AssignContributor + { + Actor = actor, + AppId = CurrentJob.AppId, + ContributorId = actor.Identifier, + IsRestore = true, + Role = Role.Owner + }); + + Log("Assigned current user."); + } + catch (DomainException ex) + { + Log($"Failed to assign contributor: {ex.Message}"); + } + } + else + { + Log("Current user not assigned because restore was triggered by client."); + } + } + + private async Task CleanupAsync(IEnumerable handlers) + { + await Safe.DeleteAsync(backupArchiveLocation, CurrentJob.Id.ToString(), log); + + if (CurrentJob.AppId != Guid.Empty) + { + foreach (var handler in handlers) + { + await Safe.CleanupRestoreErrorAsync(handler, CurrentJob.AppId, CurrentJob.Id, log); + } + } + } + + private async Task DownloadAsync() + { + Log("Downloading Backup"); + + await backupArchiveLocation.DownloadAsync(CurrentJob.Url, CurrentJob.Id.ToString()); + + Log("Downloaded Backup"); + } + + private async Task ReadEventsAsync(BackupReader reader, IEnumerable handlers) + { + await reader.ReadEventsAsync(streamNameResolver, eventDataFormatter, async storedEvent => + { + await HandleEventAsync(reader, handlers, storedEvent.Stream, storedEvent.Event); + }); + + Log($"Reading {reader.ReadEvents} events and {reader.ReadAttachments} attachments completed.", true); + } + + private async Task HandleEventAsync(BackupReader reader, IEnumerable handlers, string stream, Envelope @event) + { + if (@event.Payload is SquidexEvent squidexEvent) + { + squidexEvent.Actor = CurrentJob.Actor; + } + + if (@event.Payload is AppCreated appCreated) + { + CurrentJob.AppId = appCreated.AppId.Id; + + if (!string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) + { + appCreated.Name = CurrentJob.NewAppName; + } + } + + if (@event.Payload is AppEvent appEvent && !string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) + { + appEvent.AppId = NamedId.Of(appEvent.AppId.Id, CurrentJob.NewAppName); + } + + foreach (var handler in handlers) + { + if (!await handler.RestoreEventAsync(@event, CurrentJob.AppId, reader, CurrentJob.Actor)) + { + return; + } + } + + var eventData = eventDataFormatter.ToEventData(@event, @event.Headers.CommitId()); + var eventCommit = new List { eventData }; + + await eventStore.AppendAsync(Guid.NewGuid(), stream, eventCommit); + + Log($"Read {reader.ReadEvents} events and {reader.ReadAttachments} attachments.", true); + } + + private void Log(string message, bool replace = false) + { + if (replace && CurrentJob.Log.Count > 0) + { + CurrentJob.Log[CurrentJob.Log.Count - 1] = $"{clock.GetCurrentInstant()}: {message}"; + } + else + { + CurrentJob.Log.Add($"{clock.GetCurrentInstant()}: {message}"); + } + } + + private IEnumerable CreateHandlers() + { + return serviceProvider.GetRequiredService>(); + } + + public Task> GetJobAsync() + { + return Task.FromResult>(CurrentJob); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupStateJob.cs diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs new file mode 100644 index 000000000..5d9e58f8c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Backup.State +{ + [DataContract] + public sealed class RestoreStateJob : IRestoreJob + { + [DataMember] + public string AppName { get; set; } + + [DataMember] + public Guid Id { get; set; } + + [DataMember] + public Guid AppId { get; set; } + + [DataMember] + public RefToken Actor { get; set; } + + [DataMember] + public Uri Url { get; set; } + + [DataMember] + public string? NewAppName { get; set; } + + [DataMember] + public Instant Started { get; set; } + + [DataMember] + public Instant? Stopped { get; set; } + + [DataMember] + public List Log { get; set; } = new List(); + + [DataMember] + public JobStatus Status { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs new file mode 100644 index 000000000..2fe683b8f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.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.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Comments.Guards; +using Squidex.Domain.Apps.Entities.Comments.State; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Comments +{ + public sealed class CommentsGrain : DomainObjectGrainBase, ICommentsGrain + { + private readonly IStore store; + private readonly List> events = new List>(); + private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty }; + private IPersistence persistence; + + public override CommentsState Snapshot + { + get { return snapshot; } + } + + public CommentsGrain(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(store); + + this.store = store; + } + + protected override void ApplyEvent(Envelope @event) + { + snapshot = new CommentsState { Version = snapshot.Version + 1 }; + + events.Add(@event.To()); + } + + protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion) + { + snapshot = previousSnapshot; + } + + protected override Task ReadAsync(Type type, Guid id) + { + persistence = store.WithEventSourcing(GetType(), id, ApplyEvent); + + return persistence.ReadAsync(); + } + + protected override async Task WriteAsync(Envelope[] events, long previousVersion) + { + if (events.Length > 0) + { + await persistence.WriteEventsAsync(events); + } + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateComment createComment: + return UpsertReturn(createComment, c => + { + GuardComments.CanCreate(c); + + Create(c); + + return EntityCreatedResult.Create(createComment.CommentId, Version); + }); + + case UpdateComment updateComment: + return Upsert(updateComment, c => + { + GuardComments.CanUpdate(events, c); + + Update(c); + }); + + case DeleteComment deleteComment: + return Upsert(deleteComment, c => + { + GuardComments.CanDelete(events, c); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); + } + + public void Update(UpdateComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); + } + + public void Delete(DeleteComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); + } + + public Task GetCommentsAsync(long version = EtagVersion.Any) + { + return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs new file mode 100644 index 000000000..b3c039965 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Comments.Guards +{ + public static class GuardComments + { + public static void CanCreate(CreateComment command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot create comment.", e => + { + if (string.IsNullOrWhiteSpace(command.Text)) + { + e(Not.Defined("Text"), nameof(command.Text)); + } + }); + } + + public static void CanUpdate(List> events, UpdateComment command) + { + Guard.NotNull(command); + + var comment = FindComment(events, command.CommentId); + + if (!comment.Payload.Actor.Equals(command.Actor)) + { + throw new DomainException("Comment is created by another actor."); + } + + Validate.It(() => "Cannot update comment.", e => + { + if (string.IsNullOrWhiteSpace(command.Text)) + { + e(Not.Defined("Text"), nameof(command.Text)); + } + }); + } + + public static void CanDelete(List> events, DeleteComment command) + { + Guard.NotNull(command); + + var comment = FindComment(events, command.CommentId); + + if (!comment.Payload.Actor.Equals(command.Actor)) + { + throw new DomainException("Comment is created by another actor."); + } + } + + private static Envelope FindComment(List> events, Guid commentId) + { + Envelope? result = null; + + foreach (var @event in events) + { + if (@event.Payload is CommentCreated created && created.CommentId == commentId) + { + result = @event.To(); + } + else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) + { + result = null; + } + } + + if (result == null) + { + throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain)); + } + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs diff --git a/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DiscardChanges.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs new file mode 100644 index 000000000..6f436a0be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IContentLoader contentLoader; + + public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader) + { + Guard.NotNull(scriptEngine); + Guard.NotNull(contentLoader); + + this.scriptEngine = scriptEngine; + + this.contentLoader = contentLoader; + } + + protected override async Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedContentEvent(); + + var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber()); + + SimpleMapper.Map(content, result); + + result.Data = content.Data ?? content.DataDraft; + + switch (@event.Payload) + { + case ContentCreated _: + result.Type = EnrichedContentEventType.Created; + break; + case ContentDeleted _: + result.Type = EnrichedContentEventType.Deleted; + break; + case ContentChangesPublished _: + case ContentUpdated _: + result.Type = EnrichedContentEventType.Updated; + break; + case ContentStatusChanged contentStatusChanged: + switch (contentStatusChanged.Change) + { + case StatusChange.Published: + result.Type = EnrichedContentEventType.Published; + break; + case StatusChange.Unpublished: + result.Type = EnrichedContentEventType.Unpublished; + break; + default: + result.Type = EnrichedContentEventType.StatusChanged; + break; + } + + break; + } + + result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; + + return result; + } + + protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId) + { + if (trigger.HandleAll) + { + return true; + } + + if (trigger.Schemas != null) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, @event.SchemaId)) + { + return true; + } + } + } + + return false; + } + + protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) + { + if (trigger.HandleAll) + { + return true; + } + + if (trigger.Schemas != null) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) + { + return true; + } + } + } + + return false; + } + + private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) + { + return eventId.Id == schema.SchemaId; + } + + private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) + { + return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs new file mode 100644 index 000000000..6b002ad96 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentCommandMiddleware : GrainCommandMiddleware + { + private readonly IContentEnricher contentEnricher; + private readonly IContextProvider contextProvider; + + public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher, IContextProvider contextProvider) + : base(grainFactory) + { + Guard.NotNull(contentEnricher); + Guard.NotNull(contextProvider); + + this.contentEnricher = contentEnricher; + this.contextProvider = contextProvider; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + await base.HandleAsync(context, next); + + if (context.PlainResult is IContentEntity content && NotEnriched(context)) + { + var enriched = await contentEnricher.EnrichAsync(content, contextProvider.Context); + + context.Complete(enriched); + } + } + + private static bool NotEnriched(CommandContext context) + { + return !(context.PlainResult is IEnrichedContentEntity); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs new file mode 100644 index 000000000..ebfd08243 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentEntity : IEnrichedContentEntity + { + public Guid Id { get; set; } + + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + + public long Version { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public ScheduleJob ScheduleJob { get; set; } + + public NamedContentData? Data { get; set; } + + public NamedContentData DataDraft { get; set; } + + public NamedContentData? ReferenceData { get; set; } + + public Status Status { get; set; } + + public StatusInfo[]? Nexts { get; set; } + + public string StatusColor { get; set; } + + public string SchemaName { get; set; } + + public string SchemaDisplayName { get; set; } + + public RootField[]? ReferenceFields { get; set; } + + public bool CanUpdate { get; set; } + + public bool IsPending { get; set; } + + public HashSet CacheDependencies { get; set; } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs new file mode 100644 index 000000000..af04ee31b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -0,0 +1,377 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentGrain : LogSnapshotDomainObjectGrain, IContentGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + private readonly IAppProvider appProvider; + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + private readonly IScriptEngine scriptEngine; + private readonly IContentWorkflow contentWorkflow; + + public ContentGrain( + IStore store, + ISemanticLog log, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + IContentWorkflow contentWorkflow, + IContentRepository contentRepository, + IActivationLimit limit) + : base(store, log) + { + Guard.NotNull(appProvider); + Guard.NotNull(scriptEngine); + Guard.NotNull(assetRepository); + Guard.NotNull(contentWorkflow); + Guard.NotNull(contentRepository); + + this.appProvider = appProvider; + this.scriptEngine = scriptEngine; + this.assetRepository = assetRepository; + this.contentWorkflow = contentWorkflow; + this.contentRepository = contentRepository; + + limit?.SetLimit(5000, Lifetime); + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateContent createContent: + return CreateReturnAsync(createContent, async c => + { + var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, c, () => "Failed to create content."); + + var status = (await contentWorkflow.GetInitialStatusAsync(ctx.Schema)).Status; + + await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); + + c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, + new ScriptContext + { + Operation = "Create", + Data = c.Data, + Status = status, + StatusOld = default + }); + + await ctx.EnrichAsync(c.Data); + + if (!c.DoNotValidate) + { + await ctx.ValidateAsync(c.Data); + } + + if (c.Publish) + { + await ctx.ExecuteScriptAsync(s => s.Change, + new ScriptContext + { + Operation = "Published", + Data = c.Data, + Status = Status.Published, + StatusOld = default + }); + } + + Create(c, status); + + return Snapshot; + }); + + case UpdateContent updateContent: + return UpdateReturnAsync(updateContent, async c => + { + var isProposal = c.AsDraft && Snapshot.Status == Status.Published; + + await GuardContent.CanUpdate(Snapshot, contentWorkflow, c, isProposal); + + return await UpdateAsync(c, x => c.Data, false, isProposal); + }); + + case PatchContent patchContent: + return UpdateReturnAsync(patchContent, async c => + { + var isProposal = IsProposal(c); + + await GuardContent.CanPatch(Snapshot, contentWorkflow, c, isProposal); + + return await UpdateAsync(c, c.Data.MergeInto, true, isProposal); + }); + + case ChangeContentStatus changeContentStatus: + return UpdateReturnAsync(changeContentStatus, async c => + { + try + { + var isChangeConfirm = IsConfirm(c); + + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to change content."); + + await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c, isChangeConfirm); + + if (c.DueTime.HasValue) + { + ScheduleStatus(c); + } + else + { + if (isChangeConfirm) + { + ConfirmChanges(c); + } + else + { + var change = GetChange(c); + + await ctx.ExecuteScriptAsync(s => s.Change, + new ScriptContext + { + Operation = change.ToString(), + Data = Snapshot.Data, + Status = c.Status, + StatusOld = Snapshot.Status + }); + + ChangeStatus(c, change); + } + } + } + catch (Exception) + { + if (c.JobId.HasValue && Snapshot?.ScheduleJob?.Id == c.JobId) + { + CancelScheduling(c); + } + else + { + throw; + } + } + + return Snapshot; + }); + + case DiscardChanges discardChanges: + return UpdateReturn(discardChanges, c => + { + GuardContent.CanDiscardChanges(Snapshot.IsPending, c); + + DiscardChanges(c); + + return Snapshot; + }); + + case DeleteContent deleteContent: + return UpdateAsync(deleteContent, async c => + { + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete content."); + + GuardContent.CanDelete(ctx.Schema, c); + + await ctx.ExecuteScriptAsync(s => s.Delete, + new ScriptContext + { + Operation = "Delete", + Data = Snapshot.Data, + Status = Snapshot.Status, + StatusOld = default + }); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial, bool isProposal) + { + var currentData = + isProposal ? + Snapshot.DataDraft : + Snapshot.Data; + + var newData = newDataFunc(currentData!); + + if (!currentData!.Equals(newData)) + { + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, command, () => "Failed to update content."); + + if (partial) + { + await ctx.ValidatePartialAsync(command.Data); + } + else + { + await ctx.ValidateAsync(command.Data); + } + + newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, + new ScriptContext + { + Operation = "Create", + Data = newData, + DataOld = currentData, + Status = Snapshot.Status, + StatusOld = default + }); + + if (isProposal) + { + ProposeUpdate(command, newData); + } + else + { + Update(command, newData); + } + } + + return Snapshot; + } + + public void Create(CreateContent command, Status status) + { + RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); + + if (command.Publish) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); + } + } + + public void ConfirmChanges(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentChangesPublished())); + } + + public void DiscardChanges(DiscardChanges command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentChangesDiscarded())); + } + + public void Delete(DeleteContent command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + } + + public void Update(ContentCommand command, NamedContentData data) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = data })); + } + + public void ProposeUpdate(ContentCommand command, NamedContentData data) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdateProposed { Data = data })); + } + + public void CancelScheduling(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); + } + + public void ScheduleStatus(ChangeContentStatus command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime!.Value })); + } + + public void ChangeStatus(ChangeContentStatus command, StatusChange change) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); + } + + private void RaiseEvent(SchemaEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + if (@event.SchemaId == null) + { + @event.SchemaId = Snapshot.SchemaId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private bool IsConfirm(ChangeContentStatus command) + { + return Snapshot.IsPending && Snapshot.Status == Status.Published && command.Status == Status.Published; + } + + private bool IsProposal(PatchContent command) + { + return Snapshot.Status == Status.Published && command.AsDraft; + } + + private StatusChange GetChange(ChangeContentStatus command) + { + var change = StatusChange.Change; + + if (command.Status == Status.Published) + { + change = StatusChange.Published; + } + else if (Snapshot.Status == Status.Published) + { + change = StatusChange.Unpublished; + } + + return change; + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Content has already been deleted."); + } + } + + private async Task CreateContext(Guid appId, Guid schemaId, ContentCommand command, Func message) + { + var operationContext = + await ContentOperationContext.CreateAsync(appId, schemaId, command, + appProvider, assetRepository, contentRepository, scriptEngine, message); + + return operationContext; + } + + public Task> GetStateAsync(long version = EtagVersion.Any) + { + return J.AsTask(GetSnapshot(version)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs new file mode 100644 index 000000000..b572f431b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase + { + public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "created {[Schema]} content."); + + AddEventMessage( + "updated {[Schema]} content."); + + AddEventMessage( + "deleted {[Schema]} content."); + + AddEventMessage( + "discarded pending changes of {[Schema]} content."); + + AddEventMessage( + "published changes of {[Schema]} content."); + + AddEventMessage( + "proposed update for {[Schema]} content."); + + AddEventMessage( + "failed to schedule status change for {[Schema]} content."); + + AddEventMessage( + "changed status of {[Schema]} content to {[Status]}."); + + AddEventMessage( + "scheduled to change status of {[Schema]} content to {[Status]}."); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + var channel = $"contents.{@event.Headers.AggregateId()}"; + + HistoryEvent? result = ForEvent(@event.Payload, channel); + + if (@event.Payload is SchemaEvent schemaEvent) + { + result = result.Param("Schema", schemaEvent.SchemaId.Name); + } + + if (@event.Payload is ContentStatusChanged contentStatusChanged) + { + result = result.Param("Status", contentStatusChanged.Status); + } + + if (@event.Payload is ContentStatusScheduled contentStatusScheduled) + { + result = result.Param("Status", contentStatusScheduled.Status); + } + + return Task.FromResult(result); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs new file mode 100644 index 000000000..c1c27a52e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -0,0 +1,153 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentOperationContext + { + private IContentRepository contentRepository; + private IAssetRepository assetRepository; + private IScriptEngine scriptEngine; + private ISchemaEntity schemaEntity; + private IAppEntity appEntity; + private ContentCommand command; + private Guid schemaId; + private Func message; + + public ISchemaEntity Schema + { + get { return schemaEntity; } + } + + public static async Task CreateAsync( + Guid appId, + Guid schemaId, + ContentCommand command, + IAppProvider appProvider, + IAssetRepository assetRepository, + IContentRepository contentRepository, + IScriptEngine scriptEngine, + Func message) + { + var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(appId, schemaId); + + if (appEntity == null) + { + throw new InvalidOperationException("Cannot resolve app."); + } + + if (schemaEntity == null) + { + throw new InvalidOperationException("Cannot resolve schema."); + } + + var context = new ContentOperationContext + { + appEntity = appEntity, + assetRepository = assetRepository, + command = command, + contentRepository = contentRepository, + message = message, + schemaId = schemaId, + schemaEntity = schemaEntity, + scriptEngine = scriptEngine + }; + + return context; + } + + public Task EnrichAsync(NamedContentData data) + { + data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); + + return TaskHelper.Done; + } + + public Task ValidateAsync(NamedContentData data) + { + var ctx = CreateValidationContext(); + + return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + } + + public Task ValidatePartialAsync(NamedContentData data) + { + var ctx = CreateValidationContext(); + + return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + } + + public Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) + { + Enrich(context); + + var result = scriptEngine.ExecuteAndTransform(context, GetScript(script)); + + return Task.FromResult(result); + } + + public Task ExecuteScriptAsync(Func script, ScriptContext context) + { + Enrich(context); + + scriptEngine.Execute(context, GetScript(script)); + + return TaskHelper.Done; + } + + private void Enrich(ScriptContext context) + { + context.ContentId = command.ContentId; + + context.User = command.User; + } + + private ValidationContext CreateValidationContext() + { + return new ValidationContext(command.ContentId, schemaId, + QueryContentsAsync, + QueryContentsAsync, + QueryAssetsAsync); + } + + private async Task> QueryAssetsAsync(IEnumerable assetIds) + { + return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); + } + + private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) + { + return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); + } + + private async Task> QueryContentsAsync(HashSet ids) + { + return await contentRepository.QueryIdsAsync(appEntity.Id, ids); + } + + private string GetScript(Func script) + { + return script(schemaEntity.SchemaDef.Scripts); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs new file mode 100644 index 000000000..c63c25a81 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// 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 NodaTime; +using Orleans; +using Orleans.Runtime; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable + { + private readonly IContentRepository contentRepository; + private readonly ICommandBus commandBus; + private readonly IClock clock; + private readonly ISemanticLog log; + private TaskScheduler scheduler; + + public ContentSchedulerGrain( + IContentRepository contentRepository, + ICommandBus commandBus, + IClock clock, + ISemanticLog log) + { + Guard.NotNull(contentRepository); + Guard.NotNull(commandBus); + Guard.NotNull(clock); + Guard.NotNull(log); + + this.clock = clock; + + this.commandBus = commandBus; + this.contentRepository = contentRepository; + + this.log = log; + } + + public override Task OnActivateAsync() + { + scheduler = TaskScheduler.Current; + + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => PublishAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task PublishAsync() + { + var now = clock.GetCurrentInstant(); + + return contentRepository.QueryScheduledWithoutDataAsync(now, content => + { + return Dispatch(async () => + { + try + { + var job = content.ScheduleJob; + + if (job != null) + { + var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id }; + + await commandBus.PublishAsync(command); + } + } + catch (Exception ex) + { + log.LogError(ex, content.Id.ToString(), (logContentId, w) => w + .WriteProperty("action", "ChangeStatusScheduled") + .WriteProperty("status", "Failed") + .WriteProperty("contentId", logContentId)); + } + }); + }); + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + private Task Dispatch(Func task) + { + return Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler ?? TaskScheduler.Default).Unwrap(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs new file mode 100644 index 000000000..bc3d4344a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class DefaultWorkflowsValidator : IWorkflowsValidator + { + private readonly IAppProvider appProvider; + + public DefaultWorkflowsValidator(IAppProvider appProvider) + { + Guard.NotNull(appProvider); + + this.appProvider = appProvider; + } + + public async Task> ValidateAsync(Guid appId, Workflows workflows) + { + Guard.NotNull(workflows); + + var errors = new List(); + + if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) + { + errors.Add("Multiple workflows cover all schemas."); + } + + var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); + + foreach (var schemaId in uniqueSchemaIds) + { + if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId); + + if (schema != null) + { + errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows."); + } + } + } + + return errors; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs new file mode 100644 index 000000000..72b45c8ca --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -0,0 +1,153 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class DynamicContentWorkflow : IContentWorkflow + { + private readonly IScriptEngine scriptEngine; + private readonly IAppProvider appProvider; + + public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) + { + Guard.NotNull(scriptEngine); + Guard.NotNull(appProvider); + + this.scriptEngine = scriptEngine; + + this.appProvider = appProvider; + } + + public async Task GetAllAsync(ISchemaEntity schema) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); + } + + public async Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user); + } + + public async Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user); + } + + public async Task CanUpdateAsync(IContentEntity content) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + if (workflow.TryGetStep(content.Status, out var step)) + { + return !step.NoUpdate; + } + + return true; + } + + public async Task GetInfoAsync(IContentEntity content) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + if (workflow.TryGetStep(content.Status, out var step)) + { + return new StatusInfo(content.Status, GetColor(step)); + } + + return new StatusInfo(content.Status, StatusColors.Draft); + } + + public async Task GetInitialStatusAsync(ISchemaEntity schema) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + var (status, step) = workflow.GetInitialStep(); + + return new StatusInfo(status, GetColor(step)); + } + + public async Task GetNextsAsync(IContentEntity content, ClaimsPrincipal user) + { + var result = new List(); + + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + + foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) + { + if (CanUse(transition, content.DataDraft, user)) + { + result.Add(new StatusInfo(to, GetColor(step))); + } + } + + return result.ToArray(); + } + + private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user) + { + if (transition.Roles != null) + { + if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && transition.Roles.Contains(x.Value))) + { + return false; + } + } + + if (!string.IsNullOrWhiteSpace(transition.Expression)) + { + return scriptEngine.Evaluate("data", data, transition.Expression); + } + + return true; + } + + private async Task GetWorkflowAsync(Guid appId, Guid schemaId) + { + Workflow? result = null; + + var app = await appProvider.GetAppAsync(appId); + + if (app != null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); + + if (result == null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); + } + } + + if (result == null) + { + result = Workflow.Default; + } + + return result; + } + + private static string GetColor(WorkflowStep step) + { + return step.Color ?? StatusColors.Draft; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs new file mode 100644 index 000000000..a58359245 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private readonly IDependencyResolver resolver; + + public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) + : base(cache) + { + Guard.NotNull(resolver); + + this.resolver = resolver; + } + + public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries) + { + Guard.NotNull(context); + Guard.NotNull(queries); + + var model = await GetModelAsync(context.App); + + var ctx = new GraphQLExecutionContext(context, resolver); + + var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); + + return (result.Any(x => x.HasError), result.Map(x => x.Response)); + } + + public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query) + { + Guard.NotNull(context); + Guard.NotNull(query); + + var model = await GetModelAsync(context.App); + + var ctx = new GraphQLExecutionContext(context, resolver); + + var result = await QueryInternalAsync(model, ctx, query); + + return result; + } + + private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) + { + if (string.IsNullOrWhiteSpace(query.Query)) + { + return (false, new { data = new object() }); + } + + var (data, errors) = await model.ExecuteAsync(ctx, query); + + if (errors?.Any() == true) + { + return (false, new { data, errors }); + } + else + { + return (false, new { data }); + } + } + + private Task GetModelAsync(IAppEntity app) + { + var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); + + return Cache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); + + return new GraphQLModel(app, + allSchemas, + GetPageSizeForContents(), + GetPageSizeForAssets(), + resolver.Resolve()); + }); + } + + private int GetPageSizeForContents() + { + return resolver.Resolve>().Value.DefaultPageSizeGraphQl; + } + + private int GetPageSizeForAssets() + { + return resolver.Resolve>().Value.DefaultPageSizeGraphQl; + } + + private static object CreateCacheKey(Guid appId, string etag) + { + return $"GraphQLModel_{appId}_{etag}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs new file mode 100644 index 000000000..db16f194d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using GraphQL; +using GraphQL.DataLoader; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Contents.Queries; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLExecutionContext : QueryExecutionContext + { + private static readonly List EmptyAssets = new List(); + private static readonly List EmptyContents = new List(); + private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; + private readonly IDependencyResolver resolver; + + public IGraphQLUrlGenerator UrlGenerator { get; } + + public ISemanticLog Log { get; } + + public GraphQLExecutionContext(Context context, IDependencyResolver resolver) + : base(context, + resolver.Resolve(), + resolver.Resolve()) + { + UrlGenerator = resolver.Resolve(); + + dataLoaderContextAccessor = resolver.Resolve(); + + this.resolver = resolver; + } + + public void Setup(ExecutionOptions execution) + { + var loader = resolver.Resolve(); + + execution.Listeners.Add(loader); + execution.FieldMiddleware.Use(Middlewares.Logging(resolver.Resolve())); + execution.FieldMiddleware.Use(Middlewares.Errors()); + + execution.UserContext = this; + } + + public override async Task FindAssetAsync(Guid id) + { + var dataLoader = GetAssetsLoader(); + + return await dataLoader.LoadAsync(id); + } + + public async Task FindContentAsync(Guid id) + { + var dataLoader = GetContentsLoader(); + + return await dataLoader.LoadAsync(id); + } + + public async Task> GetReferencedAssetsAsync(IJsonValue value) + { + var ids = ParseIds(value); + + if (ids == null) + { + return EmptyAssets; + } + + var dataLoader = GetAssetsLoader(); + + return await dataLoader.LoadManyAsync(ids); + } + + public async Task> GetReferencedContentsAsync(IJsonValue value) + { + var ids = ParseIds(value); + + if (ids == null) + { + return EmptyContents; + } + + var dataLoader = GetContentsLoader(); + + return await dataLoader.LoadManyAsync(ids); + } + + private IDataLoader GetAssetsLoader() + { + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", + async batch => + { + var result = await GetReferencedAssetsAsync(new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + } + + private IDataLoader GetContentsLoader() + { + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"References", + async batch => + { + var result = await GetReferencedContentsAsync(new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + } + + private static ICollection? ParseIds(IJsonValue value) + { + try + { + var result = new List(); + + if (value is JsonArray array) + { + foreach (var id in array) + { + result.Add(Guid.Parse(id.ToString())); + } + } + + return result; + } + catch + { + return null; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs new file mode 100644 index 000000000..4f27ea7ce --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.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.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using GraphQLSchema = GraphQL.Types.Schema; + +#pragma warning disable IDE0003 + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLModel : IGraphModel + { + private readonly Dictionary contentTypes = new Dictionary(); + private readonly PartitionResolver partitionResolver; + private readonly IAppEntity app; + private readonly IObjectGraphType assetType; + private readonly IGraphType assetListType; + private readonly GraphQLSchema graphQLSchema; + + public bool CanGenerateAssetSourceUrl { get; } + + public GraphQLModel(IAppEntity app, + IEnumerable schemas, + int pageSizeContents, + int pageSizeAssets, + IGraphQLUrlGenerator urlGenerator) + { + this.app = app; + + partitionResolver = app.PartitionResolver(); + + CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; + + assetType = new AssetGraphType(this); + assetListType = new ListGraphType(new NonNullGraphType(assetType)); + + var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); + + BuildSchemas(allSchemas); + + graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); + graphQLSchema.RegisterValueConverter(JsonConverter.Instance); + + InitializeContentTypes(); + } + + private void BuildSchemas(List allSchemas) + { + foreach (var schema in allSchemas) + { + contentTypes[schema.Id] = new ContentGraphType(schema); + } + } + + private void InitializeContentTypes() + { + foreach (var contentType in contentTypes.Values) + { + contentType.Initialize(this); + } + + foreach (var contentType in contentTypes.Values) + { + graphQLSchema.RegisterType(contentType); + } + } + + private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List schemas) + { + var schema = new GraphQLSchema + { + Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) + }; + + return schema; + } + + public IFieldResolver ResolveAssetUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveAssetSourceUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveAssetThumbnailUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveContentUrl(ISchemaEntity schema) + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); + }); + + return resolver; + } + + public IFieldPartitioning ResolvePartition(Partitioning key) + { + return partitionResolver(key); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) + { + return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); + } + + public IObjectGraphType GetAssetType() + { + return assetType as IObjectGraphType; + } + + public IObjectGraphType GetContentType(Guid schemaId) + { + return contentTypes.GetOrDefault(schemaId); + } + + public async Task<(object Data, object[]? Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) + { + Guard.NotNull(context, nameof(context)); + + var result = await new DocumentExecuter().ExecuteAsync(execution => + { + context.Setup(execution); + + execution.Schema = graphQLSchema; + execution.Inputs = query.Variables?.ToInputs(); + execution.Query = query.Query; + }).ConfigureAwait(false); + + return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs new file mode 100644 index 000000000..eabe2f8b8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.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 GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphModel + { + bool CanGenerateAssetSourceUrl { get; } + + IFieldPartitioning ResolvePartition(Partitioning key); + + IFieldResolver ResolveAssetUrl(); + + IFieldResolver ResolveAssetSourceUrl(); + + IFieldResolver ResolveAssetThumbnailUrl(); + + IFieldResolver ResolveContentUrl(ISchemaEntity schema); + + IObjectGraphType GetAssetType(); + + IObjectGraphType GetContentType(Guid schemaId); + + (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs new file mode 100644 index 000000000..8be64d064 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphQLUrlGenerator + { + bool CanGenerateAssetSourceUrl { get; } + + string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); + + string? GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); + + string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); + + string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs new file mode 100644 index 000000000..86bd5e74f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL; +using GraphQL.Instrumentation; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public static class Middlewares + { + public static Func Logging(ISemanticLog log) + { + Guard.NotNull(log); + + return next => + { + return async context => + { + try + { + return await next(context); + } + catch (Exception ex) + { + log.LogWarning(ex, w => w + .WriteProperty("action", "reolveField") + .WriteProperty("status", "failed") + .WriteProperty("field", context.FieldName)); + + throw; + } + }; + }; + } + + public static Func Errors() + { + return next => + { + return async context => + { + try + { + return await next(context); + } + catch (DomainException ex) + { + throw new ExecutionError(ex.Message); + } + }; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs new file mode 100644 index 000000000..6dde0c175 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -0,0 +1,194 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class AssetGraphType : ObjectGraphType + { + public AssetGraphType(IGraphModel model) + { + Name = "Asset"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = AllTypes.NonNullGuid, + Resolver = Resolve(x => x.Id.ToString()), + Description = "The id of the asset." + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = "The version of the asset." + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.Created), + Description = "The date and time when the asset has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = "The user that has created the asset." + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.LastModified), + Description = "The date and time when the asset has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = "The user that has updated the asset last." + }); + + AddField(new FieldType + { + Name = "mimeType", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.MimeType), + Description = "The mime type." + }); + + AddField(new FieldType + { + Name = "url", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveAssetUrl(), + Description = "The url to the asset." + }); + + AddField(new FieldType + { + Name = "thumbnailUrl", + ResolvedType = AllTypes.String, + Resolver = model.ResolveAssetThumbnailUrl(), + Description = "The thumbnail url to the asset." + }); + + AddField(new FieldType + { + Name = "fileName", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.FileName), + Description = "The file name." + }); + + AddField(new FieldType + { + Name = "fileHash", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.FileHash), + Description = "The hash of the file. Can be null for old files." + }); + + AddField(new FieldType + { + Name = "fileType", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.FileName.FileType()), + Description = "The file type." + }); + + AddField(new FieldType + { + Name = "fileSize", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.FileSize), + Description = "The size of the file in bytes." + }); + + AddField(new FieldType + { + Name = "fileVersion", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.FileVersion), + Description = "The version of the file." + }); + + AddField(new FieldType + { + Name = "slug", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Slug), + Description = "The file name as slug." + }); + + AddField(new FieldType + { + Name = "isImage", + ResolvedType = AllTypes.NonNullBoolean, + Resolver = Resolve(x => x.IsImage), + Description = "Determines of the created file is an image." + }); + + AddField(new FieldType + { + Name = "pixelWidth", + ResolvedType = AllTypes.Int, + Resolver = Resolve(x => x.PixelWidth), + Description = "The width of the image in pixels if the asset is an image." + }); + + AddField(new FieldType + { + Name = "pixelHeight", + ResolvedType = AllTypes.Int, + Resolver = Resolve(x => x.PixelHeight), + Description = "The height of the image in pixels if the asset is an image." + }); + + AddField(new FieldType + { + Name = "tags", + ResolvedType = null, + Resolver = Resolve(x => x.TagNames), + Description = "The asset tags.", + Type = AllTypes.NonNullTagsType + }); + + if (model.CanGenerateAssetSourceUrl) + { + AddField(new FieldType + { + Name = "sourceUrl", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveAssetSourceUrl(), + Description = "The source url of the asset." + }); + } + + Description = "An asset"; + } + + private static IFieldResolver Resolve(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs new file mode 100644 index 000000000..97e299469 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentDataGraphType : ObjectGraphType + { + public ContentDataGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) + { + Name = $"{schemaType}DataDto"; + + foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) + { + var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); + + if (valueResolver != null) + { + var displayName = field.DisplayName(); + + var fieldGraphType = new ObjectGraphType + { + Name = $"{schemaType}Data{typeName}Dto" + }; + + var partition = model.ResolvePartition(field.Partitioning); + + foreach (var partitionItem in partition) + { + var key = partitionItem.Key; + + fieldGraphType.AddField(new FieldType + { + Name = key.EscapePartition(), + Resolver = PartitionResolver(valueResolver, key), + ResolvedType = resolvedType, + Description = field.RawProperties.Hints + }); + } + + fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content type."; + + AddField(new FieldType + { + Name = fieldName, + Resolver = FieldResolver(field), + ResolvedType = fieldGraphType, + Description = $"The {displayName} field." + }); + } + } + + Description = $"The structure of the {schemaName} content type."; + } + + private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) + { + return new FuncFieldResolver(c => + { + if (((ContentFieldData)c.Source).TryGetValue(key, out var value) && value != null) + { + return valueResolver(value, c); + } + else + { + return null; + } + }); + } + + private static FuncFieldResolver?> FieldResolver(RootField field) + { + return new FuncFieldResolver?>(c => + { + return c.Source?.GetOrDefault(field.Name); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs new file mode 100644 index 000000000..b0140d917 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentGraphType : ObjectGraphType + { + private readonly ISchemaEntity schema; + private readonly string schemaType; + private readonly string schemaName; + + public ContentGraphType(ISchemaEntity schema) + { + this.schema = schema; + + schemaType = schema.TypeName(); + schemaName = schema.DisplayName(); + + Name = $"{schemaType}"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = AllTypes.NonNullGuid, + Resolver = Resolve(x => x.Id), + Description = $"The id of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = $"The version of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.Created), + Description = $"The date and time when the {schemaName} content has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = $"The user that has created the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.LastModified), + Description = $"The date and time when the {schemaName} content has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = $"The user that has updated the {schemaName} content last." + }); + + AddField(new FieldType + { + Name = "status", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), + Description = $"The the status of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "statusColor", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.StatusColor), + Description = $"The color status of the {schemaName} content." + }); + + Interface(); + + Description = $"The structure of a {schemaName} content type."; + + IsTypeOf = CheckType; + } + + private bool CheckType(object value) + { + return value is IContentEntity content && content.SchemaId?.Id == schema.Id; + } + + public void Initialize(IGraphModel model) + { + AddField(new FieldType + { + Name = "url", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveContentUrl(schema), + Description = $"The url to the the {schemaName} content." + }); + + var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model); + + if (contentDataType.Fields.Any()) + { + AddField(new FieldType + { + Name = "data", + ResolvedType = new NonNullGraphType(contentDataType), + Resolver = Resolve(x => x.Data), + Description = $"The data of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "dataDraft", + ResolvedType = contentDataType, + Resolver = Resolve(x => x.DataDraft), + Description = $"The draft data of the {schemaName} content." + }); + } + } + + private static IFieldResolver Resolve(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs new file mode 100644 index 000000000..53b92df13 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Types; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentUnionGraphType : UnionGraphType + { + private readonly Dictionary types = new Dictionary(); + + public ContentUnionGraphType(string fieldName, Dictionary schemaTypes, IEnumerable? schemaIds) + { + Name = $"{fieldName}ReferenceUnionDto"; + + if (schemaIds?.Any() == true) + { + foreach (var schemaId in schemaIds) + { + var schemaType = schemaTypes.GetOrDefault(schemaId); + + if (schemaType != null) + { + types[schemaId] = schemaType; + } + } + } + else + { + foreach (var schemaType in schemaTypes) + { + types[schemaType.Key] = schemaType.Value; + } + } + + foreach (var type in types) + { + AddPossibleType(type.Value); + } + + ResolveType = value => + { + if (value is IContentEntity content) + { + return types.GetOrDefault(content.SchemaId.Id); + } + + return null; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs new file mode 100644 index 000000000..7f90e1f52 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class NestedGraphType : ObjectGraphType + { + public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName) + { + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + + var fieldDisplayName = field.DisplayName(); + + Name = $"{schemaType}{fieldName}ChildDto"; + + foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) + { + var fieldInfo = model.GetGraphType(schema, nestedField, nestedName); + + if (fieldInfo.ResolveType != null && fieldInfo.Resolver != null) + { + var resolver = ValueResolver(nestedField, fieldInfo.Resolver); + + AddField(new FieldType + { + Name = nestedName, + Resolver = resolver, + ResolvedType = fieldInfo.ResolveType, + Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." + }); + } + } + + Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; + } + + private static FuncFieldResolver ValueResolver(NestedField nestedField, ValueResolver resolver) + { + return new FuncFieldResolver(c => + { + if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value)) + { + return resolver(value, c); + } + else + { + return null; + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs new file mode 100644 index 000000000..5867a7f07 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context); + + public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType? ResolveType, ValueResolver? Resolver)> + { + private static readonly ValueResolver NoopResolver = (value, c) => value; + private readonly Dictionary schemaTypes; + private readonly ISchemaEntity schema; + private readonly IGraphModel model; + private readonly IGraphType assetListType; + private readonly string fieldName; + + public QueryGraphTypeVisitor(ISchemaEntity schema, + Dictionary schemaTypes, + IGraphModel model, + IGraphType assetListType, + string fieldName) + { + this.model = model; + this.assetListType = assetListType; + this.schema = schema; + this.schemaTypes = schemaTypes; + this.fieldName = fieldName; + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IArrayField field) + { + return ResolveNested(field); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveAssets(); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopBoolean); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopDate); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopGeolocation); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopJson); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopFloat); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveReferences(field); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopString); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return ResolveDefault(AllTypes.NoopTags); + } + + public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField field) + { + return (null, null); + } + + private static (IGraphType? ResolveType, ValueResolver? Resolver) ResolveDefault(IGraphType type) + { + return (type, NoopResolver); + } + + private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveNested(IArrayField field) + { + var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); + + return (schemaFieldType, NoopResolver); + } + + private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveAssets() + { + var resolver = new ValueResolver((value, c) => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.GetReferencedAssetsAsync(value); + }); + + return (assetListType, resolver); + } + + private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveReferences(IField field) + { + IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); + + if (contentType == null) + { + var union = new ContentUnionGraphType(fieldName, schemaTypes, field.Properties.SchemaIds); + + if (!union.PossibleTypes.Any()) + { + return (null, null); + } + + contentType = union; + } + + var resolver = new ValueResolver((value, c) => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return context.GetReferencedContentsAsync(value); + }); + + var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); + + return (schemaFieldType, resolver); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs new file mode 100644 index 000000000..a19703dbd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Language.AST; +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class GuidGraphType2 : ScalarGraphType + { + public GuidGraphType2() + { + Name = "Guid"; + + Description = "The `Guid` scalar type global unique identifier"; + } + + public override object? Serialize(object value) + { + return ParseValue(value)?.ToString(); + } + + public override object? ParseValue(object value) + { + if (value is Guid guid) + { + return guid; + } + + var inputValue = value?.ToString()?.Trim('"'); + + if (Guid.TryParse(inputValue, out guid)) + { + return guid; + } + + return null; + } + + public override object? ParseLiteral(IValue value) + { + if (value is StringValue stringValue) + { + return ParseValue(stringValue.Value); + } + + return null; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs new file mode 100644 index 000000000..bec2558a2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; +using NodaTime.Text; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class InstantGraphType : DateGraphType + { + public override object Serialize(object value) + { + return ParseValue(value); + } + + public override object ParseValue(object value) + { + return InstantPattern.General.Parse(value.ToString()).Value; + } + + public override object? ParseLiteral(IValue value) + { + if (value is InstantValue timeValue) + { + return ParseValue(timeValue.Value); + } + + if (value is StringValue stringValue) + { + return ParseValue(stringValue.Value); + } + + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs new file mode 100644 index 000000000..ea597a4b3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class JsonConverter : IAstFromValueConverter + { + public static readonly JsonConverter Instance = new JsonConverter(); + + private JsonConverter() + { + } + + public IValue Convert(object value, IGraphType type) + { + return new JsonValueNode(value as JsonObject ?? JsonValue.Null); + } + + public bool Matches(object value, IGraphType type) + { + return value is JsonObject; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs new file mode 100644 index 000000000..6540fd92a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class JsonGraphType : ScalarGraphType + { + public JsonGraphType() + { + Name = "Json"; + + Description = "Unstructured Json object"; + } + + public override object Serialize(object value) + { + return value; + } + + public override object ParseValue(object value) + { + return value; + } + + public override object ParseLiteral(IValue value) + { + if (value is JsonValueNode jsonGraphType) + { + return jsonGraphType.Value; + } + + return value; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs new file mode 100644 index 000000000..7ac033df6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class JsonValueNode : ValueNode + { + public JsonValueNode(IJsonValue value) + { + Value = value; + } + + protected override bool Equals(ValueNode node) + { + return false; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs new file mode 100644 index 000000000..9e346fe42 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.Guards +{ + public static class GuardContent + { + public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot created content.", e => + { + ValidateData(command, e); + }); + + if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id) + { + throw new DomainException("Singleton content cannot be created."); + } + + if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User)) + { + throw new DomainException("Content workflow prevents publishing."); + } + } + + public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command, bool isProposal) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot update content.", e => + { + ValidateData(command, e); + }); + + if (!isProposal) + { + await ValidateCanUpdate(content, contentWorkflow); + } + } + + public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command, bool isProposal) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot patch content.", e => + { + ValidateData(command, e); + }); + + if (!isProposal) + { + await ValidateCanUpdate(content, contentWorkflow); + } + } + + public static void CanDiscardChanges(bool isPending, DiscardChanges command) + { + Guard.NotNull(command); + + if (!isPending) + { + throw new DomainException("The content has no pending changes."); + } + } + + public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command, bool isChangeConfirm) + { + Guard.NotNull(command); + + if (schema.SchemaDef.IsSingleton && command.Status != Status.Published) + { + throw new DomainException("Singleton content cannot be changed."); + } + + return Validate.It(() => "Cannot change status.", async e => + { + if (isChangeConfirm) + { + if (!content.IsPending) + { + e("Content has no changes to publish.", nameof(command.Status)); + } + } + else if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User)) + { + e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); + } + + if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) + { + e("Due time must be in the future.", nameof(command.DueTime)); + } + }); + } + + public static void CanDelete(ISchemaEntity schema, DeleteContent command) + { + Guard.NotNull(command); + + if (schema.SchemaDef.IsSingleton) + { + throw new DomainException("Singleton content cannot be deleted."); + } + } + + private static void ValidateData(ContentDataCommand command, AddValidation e) + { + if (command.Data == null) + { + e(Not.Defined("Data"), nameof(command.Data)); + } + } + + private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow) + { + if (!await contentWorkflow.CanUpdateAsync(content)) + { + throw new DomainException($"The workflow does not allow updates at status {content.Status}"); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs new file mode 100644 index 000000000..ed1919e63 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + NamedId AppId { get; } + + NamedId SchemaId { get; } + + Status Status { get; } + + ScheduleJob? ScheduleJob { get; } + + NamedContentData? Data { get; } + + NamedContentData DataDraft { get; } + + bool IsPending { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs new file mode 100644 index 000000000..618881ff2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies + { + bool CanUpdate { get; } + + string StatusColor { get; } + + string SchemaName { get; } + + string SchemaDisplayName { get; } + + RootField[]? ReferenceFields { get; } + + StatusInfo[]? Nexts { get; } + + NamedContentData? ReferenceData { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs new file mode 100644 index 000000000..fce76d51e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -0,0 +1,377 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class ContentEnricher : IContentEnricher + { + private const string DefaultColor = StatusColors.Draft; + private static readonly ILookup EmptyContents = Enumerable.Empty().ToLookup(x => x.Id); + private static readonly ILookup EmptyAssets = Enumerable.Empty().ToLookup(x => x.Id); + private readonly IAssetQueryService assetQuery; + private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly Lazy contentQuery; + private readonly IContentWorkflow contentWorkflow; + + private IContentQueryService ContentQuery + { + get { return contentQuery.Value; } + } + + public ContentEnricher(IAssetQueryService assetQuery, IAssetUrlGenerator assetUrlGenerator, Lazy contentQuery, IContentWorkflow contentWorkflow) + { + Guard.NotNull(assetQuery, nameof(assetQuery)); + Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); + + this.assetQuery = assetQuery; + this.assetUrlGenerator = assetUrlGenerator; + this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; + } + + public async Task EnrichAsync(IContentEntity content, Context context) + { + Guard.NotNull(content, nameof(content)); + + var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), context); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable contents, Context context) + { + Guard.NotNull(contents, nameof(contents)); + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var results = new List(); + + if (contents.Any()) + { + var appVersion = context.App.Version; + + var cache = new Dictionary<(Guid, Status), StatusInfo>(); + + foreach (var content in contents) + { + var result = SimpleMapper.Map(content, new ContentEntity()); + + await EnrichColorAsync(content, result, cache); + + if (ShouldEnrichWithStatuses(context)) + { + await EnrichNextsAsync(content, result, context); + await EnrichCanUpdateAsync(content, result); + } + + results.Add(result); + } + + foreach (var group in results.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + foreach (var content in group) + { + content.CacheDependencies = new HashSet + { + schema.Id, + schema.Version + }; + } + + if (ShouldEnrichWithSchema(context)) + { + var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); + + var schemaName = schema.SchemaDef.Name; + var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); + + foreach (var content in group) + { + content.ReferenceFields = referenceFields; + content.SchemaName = schemaName; + content.SchemaDisplayName = schemaDisplayName; + } + } + } + + if (ShouldEnrich(context)) + { + await EnrichReferencesAsync(context, results); + await EnrichAssetsAsync(context, results); + } + } + + return results; + } + } + + private async Task EnrichAssetsAsync(Context context, List contents) + { + var ids = new HashSet(); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + AddAssetIds(ids, schema, group); + } + + var assets = await GetAssetsAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + ResolveAssets(schema, group, assets); + } + } + + private async Task EnrichReferencesAsync(Context context, List contents) + { + var ids = new HashSet(); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + AddReferenceIds(ids, schema, group); + } + + var references = await GetReferencesAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + await ResolveReferencesAsync(context, schema, group, references); + } + } + + private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable contents, ILookup references) + { + var formatted = new Dictionary(); + + foreach (var field in schema.SchemaDef.ResolvingReferences()) + { + foreach (var content in contents) + { + if (content.ReferenceData == null) + { + content.ReferenceData = new NamedContentData(); + } + + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + + try + { + if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var referencedContents = + field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) + .Select(x => references[x]) + .SelectMany(x => x) + .ToList(); + + if (referencedContents.Count == 1) + { + var reference = referencedContents[0]; + + var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString()); + + content.CacheDependencies.Add(referencedSchema.Id); + content.CacheDependencies.Add(referencedSchema.Version); + content.CacheDependencies.Add(reference.Id); + content.CacheDependencies.Add(reference.Version); + + var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); + + fieldReference.AddJsonValue(partitionValue.Key, value); + } + else if (referencedContents.Count > 1) + { + var value = CreateFallback(context, referencedContents); + + fieldReference.AddJsonValue(partitionValue.Key, value); + } + } + } + } + catch (DomainObjectNotFoundException) + { + continue; + } + } + } + } + + private void ResolveAssets(ISchemaEntity schema, IGrouping contents, ILookup assets) + { + foreach (var field in schema.SchemaDef.ResolvingAssets()) + { + foreach (var content in contents) + { + if (content.ReferenceData == null) + { + content.ReferenceData = new NamedContentData(); + } + + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + + if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var partitionValue in fieldData) + { + var referencedImage = + field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) + .Select(x => assets[x]) + .SelectMany(x => x) + .FirstOrDefault(x => x.IsImage); + + if (referencedImage != null) + { + var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); + + content.CacheDependencies.Add(referencedImage.Id); + content.CacheDependencies.Add(referencedImage.Version); + + fieldReference.AddJsonValue(partitionValue.Key, JsonValue.Create(url)); + } + } + } + } + } + } + + private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) + { + return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); + } + + private static JsonObject CreateFallback(Context context, List referencedContents) + { + var text = $"{referencedContents.Count} Reference(s)"; + + var value = JsonValue.Object(); + + foreach (var language in context.App.LanguagesConfig) + { + value.Add(language.Key, text); + } + + return value; + } + + private void AddReferenceIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) + { + foreach (var content in contents) + { + ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly)); + } + } + + private void AddAssetIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) + { + foreach (var content in contents) + { + ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly)); + } + } + + private async Task> GetReferencesAsync(Context context, HashSet ids) + { + if (ids.Count == 0) + { + return EmptyContents; + } + + var references = await ContentQuery.QueryAsync(context.Clone().WithNoContentEnrichment(true), ids.ToList()); + + return references.ToLookup(x => x.Id); + } + + private async Task> GetAssetsAsync(Context context, HashSet ids) + { + if (ids.Count == 0) + { + return EmptyAssets; + } + + var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), Q.Empty.WithIds(ids)); + + return assets.ToLookup(x => x.Id); + } + + private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result) + { + result.CanUpdate = await contentWorkflow.CanUpdateAsync(content); + } + + private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context) + { + result.Nexts = await contentWorkflow.GetNextsAsync(content, context.User); + } + + private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) + { + result.StatusColor = await GetColorAsync(content, cache); + } + + private async Task GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) + { + if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) + { + info = await contentWorkflow.GetInfoAsync(content); + + if (info == null) + { + info = new StatusInfo(content.Status, DefaultColor); + } + + cache[(content.SchemaId.Id, content.Status)] = info; + } + + return info.Color; + } + + private static bool ShouldEnrichWithSchema(Context context) + { + return context.IsFrontendClient; + } + + private static bool ShouldEnrichWithStatuses(Context context) + { + return context.IsFrontendClient || context.IsResolveFlow(); + } + + private static bool ShouldEnrich(Context context) + { + return context.IsFrontendClient && !context.IsNoEnrichment(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs new file mode 100644 index 000000000..47ff85b87 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class ContentLoader : IContentLoader + { + private readonly IGrainFactory grainFactory; + + public ContentLoader(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid id, long version) + { + using (Profiler.TraceMethod()) + { + var grain = grainFactory.GetGrain(id); + + var content = await grain.GetStateAsync(version); + + if (content.Value == null || (version > EtagVersion.Any && content.Value.Version != version)) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); + } + + return content.Value; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs new file mode 100644 index 000000000..6bdf754c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -0,0 +1,205 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OData; +using Microsoft.OData.Edm; +using NJsonSchema; +using Squidex.Domain.Apps.Core.GenerateEdmSchema; +using Squidex.Domain.Apps.Core.GenerateJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Queries.OData; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentQueryParser : CachingProviderBase + { + private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); + private readonly IJsonSerializer jsonSerializer; + private readonly ContentOptions options; + + public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions options) + : base(cache) + { + this.jsonSerializer = jsonSerializer; + this.options = options.Value; + } + + public virtual ClrQuery ParseQuery(Context context, ISchemaEntity schema, Q q) + { + Guard.NotNull(context); + Guard.NotNull(schema); + + using (Profiler.TraceMethod()) + { + var result = new ClrQuery(); + + if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) + { + result = ParseJson(context, schema, q.JsonQuery); + } + else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + { + result = ParseOData(context, schema, q.ODataQuery); + } + + if (result.Sort.Count == 0) + { + result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); + } + + if (result.Take == long.MaxValue) + { + result.Take = options.DefaultPageSize; + } + else if (result.Take > options.MaxResults) + { + result.Take = options.MaxResults; + } + + return result; + } + } + + private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) + { + var jsonSchema = BuildJsonSchema(context, schema); + + return jsonSchema.Parse(json, jsonSerializer); + } + + private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) + { + try + { + var model = BuildEdmModel(context, schema); + + return model.ParseQuery(odata).ToQuery(); + } + catch (NotSupportedException) + { + throw new ValidationException("OData operation is not supported."); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + + private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) + { + var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheTime; + + return BuildJsonSchema(schema.SchemaDef, context.App, context.IsFrontendClient); + }); + + return result; + } + + private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) + { + var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheTime; + + return BuildEdmModel(schema.SchemaDef, context.App, context.IsFrontendClient); + }); + + return result; + } + + private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields) + { + var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); + + return new ContentSchemaBuilder().CreateContentSchema(schema, dataSchema); + } + + private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) + { + var model = new EdmModel(); + + var pascalAppName = app.Name.ToPascalCase(); + var pascalSchemaName = schema.Name.ToPascalCase(); + + var typeFactory = new EdmTypeFactory(name => + { + var finalName = pascalSchemaName; + + if (!string.IsNullOrWhiteSpace(name)) + { + finalName += "."; + finalName += name; + } + + var result = model.SchemaElements.OfType().FirstOrDefault(x => x.Name == finalName); + + if (result != null) + { + return (result, false); + } + + result = new EdmComplexType(pascalAppName, finalName); + + model.AddElement(result); + + return (result, true); + }); + + var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); + + var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); + entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("ContentSet", entityType); + + model.AddElement(container); + model.AddElement(schemaType); + model.AddElement(entityType); + + return model; + } + + private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) + { + return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; + } + + private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) + { + return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs new file mode 100644 index 000000000..bdd029137 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -0,0 +1,341 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; + +#pragma warning disable RECS0147 + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class ContentQueryService : IContentQueryService + { + private static readonly Status[] StatusPublishedOnly = { Status.Published }; + private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); + private readonly IAppProvider appProvider; + private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly IContentEnricher contentEnricher; + private readonly IContentRepository contentRepository; + private readonly IContentLoader contentVersionLoader; + private readonly IScriptEngine scriptEngine; + private readonly ContentQueryParser queryParser; + + public ContentQueryService( + IAppProvider appProvider, + IAssetUrlGenerator assetUrlGenerator, + IContentEnricher contentEnricher, + IContentRepository contentRepository, + IContentLoader contentVersionLoader, + IScriptEngine scriptEngine, + ContentQueryParser queryParser) + { + Guard.NotNull(appProvider); + Guard.NotNull(assetUrlGenerator); + Guard.NotNull(contentEnricher); + Guard.NotNull(contentRepository); + Guard.NotNull(contentVersionLoader); + Guard.NotNull(queryParser); + Guard.NotNull(scriptEngine); + + this.appProvider = appProvider; + this.assetUrlGenerator = assetUrlGenerator; + this.contentEnricher = contentEnricher; + this.contentRepository = contentRepository; + this.contentVersionLoader = contentVersionLoader; + this.queryParser = queryParser; + this.scriptEngine = scriptEngine; + this.queryParser = queryParser; + } + + public async Task FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = -1) + { + Guard.NotNull(context); + + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + + CheckPermission(context, schema); + + using (Profiler.TraceMethod()) + { + IContentEntity? content; + + if (version > EtagVersion.Empty) + { + content = await FindByVersionAsync(id, version); + } + else + { + content = await FindCoreAsync(context, id, schema); + } + + if (content == null || content.SchemaId.Id != schema.Id) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); + } + + return await TransformAsync(context, schema, content); + } + } + + public async Task> QueryAsync(Context context, string schemaIdOrName, Q query) + { + Guard.NotNull(context); + + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + + CheckPermission(context, schema); + + using (Profiler.TraceMethod()) + { + IResultList contents; + + if (query.Ids != null && query.Ids.Count > 0) + { + contents = await QueryByIdsAsync(context, schema, query); + } + else + { + contents = await QueryByQueryAsync(context, schema, query); + } + + return await TransformAsync(context, schema, contents); + } + } + + public async Task> QueryAsync(Context context, IReadOnlyList ids) + { + Guard.NotNull(context); + + using (Profiler.TraceMethod()) + { + if (ids == null || ids.Count == 0) + { + return EmptyContents; + } + + var results = new List(); + + var contents = await QueryCoreAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.Schema.Id)) + { + var schema = group.First().Schema; + + if (HasPermission(context, schema)) + { + var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content)); + + results.AddRange(enriched); + } + } + + return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); + } + } + + private async Task> TransformAsync(Context context, ISchemaEntity schema, IResultList contents) + { + var transformed = await TransformCoreAsync(context, schema, contents); + + return ResultList.Create(contents.Total, transformed); + } + + private async Task TransformAsync(Context context, ISchemaEntity schema, IContentEntity content) + { + var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1)); + + return transformed[0]; + } + + private async Task> TransformCoreAsync(Context context, ISchemaEntity schema, IEnumerable contents) + { + using (Profiler.TraceMethod()) + { + var results = new List(); + + var converters = GenerateConverters(context).ToArray(); + + var script = schema.SchemaDef.Scripts.Query; + var scripting = !string.IsNullOrWhiteSpace(script); + + var enriched = await contentEnricher.EnrichAsync(contents, context); + + foreach (var content in enriched) + { + var result = SimpleMapper.Map(content, new ContentEntity()); + + if (result.Data != null) + { + if (!context.IsFrontendClient && scripting) + { + var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; + + result.Data = scriptEngine.Transform(ctx, script); + } + + result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); + } + + if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient)) + { + result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters); + } + else + { + result.DataDraft = null!; + } + + results.Add(result); + } + + return results; + } + } + + private IEnumerable GenerateConverters(Context context) + { + if (!context.IsFrontendClient) + { + yield return FieldConverters.ExcludeHidden(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); + } + + yield return FieldConverters.ExcludeChangedTypes(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); + + yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); + yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); + + if (!context.IsFrontendClient) + { + yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig); + + var languages = context.Languages(); + + if (languages.Any()) + { + yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); + } + + var assetUrls = context.AssetUrls(); + + if (assetUrls.Any()) + { + yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator); + } + } + } + + public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) + { + ISchemaEntity? schema = null; + + if (Guid.TryParse(schemaIdOrName, out var id)) + { + schema = await appProvider.GetSchemaAsync(context.App.Id, id); + } + + if (schema == null) + { + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName); + } + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); + } + + return schema; + } + + private static void CheckPermission(Context context, params ISchemaEntity[] schemas) + { + foreach (var schema in schemas) + { + if (!HasPermission(context, schema)) + { + throw new DomainForbiddenException("You do not have permission for this schema."); + } + } + } + + private static bool HasPermission(Context context, ISchemaEntity schema) + { + var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); + + return context.Permissions.Allows(permission); + } + + private static Status[]? GetStatus(Context context) + { + if (context.IsFrontendClient || context.IsUnpublished()) + { + return null; + } + else + { + return StatusPublishedOnly; + } + } + + private async Task> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) + { + var parsedQuery = queryParser.ParseQuery(context, schema, query); + + return await QueryCoreAsync(context, schema, parsedQuery); + } + + private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) + { + var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); + + return contents.SortSet(x => x.Id, query.Ids); + } + + private Task> QueryCoreAsync(Context context, IReadOnlyList ids) + { + return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet(ids), WithDraft(context)); + } + + private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query) + { + return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context)); + } + + private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) + { + return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context)); + } + + private Task FindCoreAsync(Context context, Guid id, ISchemaEntity schema) + { + return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context)); + } + + private Task FindByVersionAsync(Guid id, long version) + { + return contentVersionLoader.GetAsync(id, version); + } + + private static bool WithDraft(Context context) + { + return context.IsUnpublished() || context.IsFrontendClient; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs new file mode 100644 index 000000000..f5cd77bf4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public sealed class FilterTagTransformer : TransformVisitor + { + private readonly ITagService tagService; + private readonly ISchemaEntity schema; + private readonly Guid appId; + + private FilterTagTransformer(Guid appId, ISchemaEntity schema, ITagService tagService) + { + this.appId = appId; + this.schema = schema; + this.tagService = tagService; + } + + public static FilterNode? Transform(FilterNode nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService) + { + Guard.NotNull(nodeIn); + Guard.NotNull(tagService); + Guard.NotNull(schema); + + return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path)) + { + var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result; + + if (tagNames.TryGetValue(stringValue, out var normalized)) + { + return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); + } + } + + return nodeIn; + } + + private static bool IsDataPath(IReadOnlyList path) + { + return path.Count == 3 && string.Equals(path[0], nameof(IContentEntity.Data), StringComparison.OrdinalIgnoreCase); + } + + private bool IsTagField(IReadOnlyList path) + { + return schema.SchemaDef.FieldsByName.TryGetValue(path[1], out var field) && IsTagField(field); + } + + private bool IsTagField(IField field) + { + return field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs new file mode 100644 index 000000000..b452e4667 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class QueryExecutionContext + { + private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); + private readonly IContentQueryService contentQuery; + private readonly IAssetQueryService assetQuery; + private readonly Context context; + + public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery) + { + Guard.NotNull(assetQuery); + Guard.NotNull(contentQuery); + Guard.NotNull(context); + + this.assetQuery = assetQuery; + this.contentQuery = contentQuery; + this.context = context; + } + + public virtual async Task FindAssetAsync(Guid id) + { + var asset = cachedAssets.GetOrDefault(id); + + if (asset == null) + { + asset = await assetQuery.FindAssetAsync(context, id); + + if (asset != null) + { + cachedAssets[asset.Id] = asset; + } + } + + return asset; + } + + public virtual async Task FindContentAsync(Guid schemaId, Guid id) + { + var content = cachedContents.GetOrDefault(id); + + if (content == null) + { + content = await contentQuery.FindContentAsync(context, schemaId.ToString(), id); + + if (content != null) + { + cachedContents[content.Id] = content; + } + } + + return content; + } + + public virtual async Task> QueryAssetsAsync(string query) + { + var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); + + foreach (var asset in assets) + { + cachedAssets[asset.Id] = asset; + } + + return assets; + } + + public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) + { + var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); + + foreach (var content in result) + { + cachedContents[content.Id] = content; + } + + return result; + } + + public virtual async Task> GetReferencedAssetsAsync(ICollection ids) + { + Guard.NotNull(ids); + + var notLoadedAssets = new HashSet(ids.Where(id => !cachedAssets.ContainsKey(id))); + + if (notLoadedAssets.Count > 0) + { + var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets)); + + foreach (var asset in assets) + { + cachedAssets[asset.Id] = asset; + } + } + + return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); + } + + public virtual async Task> GetReferencedContentsAsync(ICollection ids) + { + Guard.NotNull(ids); + + var notLoadedContents = ids.Where(id => !cachedContents.ContainsKey(id)).ToList(); + + if (notLoadedContents.Count > 0) + { + var result = await contentQuery.QueryAsync(context, notLoadedContents); + + foreach (var content in result) + { + cachedContents[content.Id] = content; + } + } + + return ids.Select(cachedContents.GetOrDefault).Where(x => x != null).ToList(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs new file mode 100644 index 000000000..1cb5655a6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// 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.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.Contents.Repositories +{ + public interface IContentRepository + { + Task> QueryAsync(IAppEntity app, Status[]? status, HashSet ids, bool includeDraft); + + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet ids, bool includeDraft); + + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, bool inDraft, ClrQuery query, bool includeDraft); + + Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); + + Task> QueryIdsAsync(Guid appId, HashSet ids); + + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft); + + Task QueryScheduledWithoutDataAsync(Instant now, Func callback); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs new file mode 100644 index 000000000..8e13695c5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Contents.State +{ + public class ContentState : DomainObjectState, IContentEntity + { + [DataMember] + public NamedId AppId { get; set; } + + [DataMember] + public NamedId SchemaId { get; set; } + + [DataMember] + public NamedContentData Data { get; set; } + + [DataMember] + public NamedContentData DataDraft { get; set; } + + [DataMember] + public ScheduleJob? ScheduleJob { get; set; } + + [DataMember] + public bool IsPending { get; set; } + + [DataMember] + public bool IsDeleted { get; set; } + + [DataMember] + public Status Status { get; set; } + + public void ApplyEvent(IEvent @event) + { + switch (@event) + { + case ContentCreated e: + { + SimpleMapper.Map(e, this); + + UpdateData(null, e.Data, false); + + break; + } + + case ContentChangesPublished _: + { + ScheduleJob = null; + + UpdateData(DataDraft, null, false); + + break; + } + + case ContentStatusChanged e: + { + ScheduleJob = null; + + SimpleMapper.Map(e, this); + + if (e.Status == Status.Published) + { + UpdateData(DataDraft, null, false); + } + + break; + } + + case ContentUpdated e: + { + UpdateData(e.Data, e.Data, false); + + break; + } + + case ContentUpdateProposed e: + { + UpdateData(null, e.Data, true); + + break; + } + + case ContentChangesDiscarded _: + { + UpdateData(null, Data, false); + + break; + } + + case ContentSchedulingCancelled _: + { + ScheduleJob = null; + + break; + } + + case ContentStatusScheduled e: + { + ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); + + break; + } + + case ContentDeleted _: + { + IsDeleted = true; + + break; + } + } + } + + public override ContentState Apply(Envelope @event) + { + return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + } + + private void UpdateData(NamedContentData? data, NamedContentData? dataDraft, bool isPending) + { + if (data != null) + { + Data = data; + } + + if (dataDraft != null) + { + DataDraft = dataDraft; + } + + IsPending = isPending; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs new file mode 100644 index 000000000..dc2263426 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class GrainTextIndexer : ITextIndexer, IEventConsumer + { + private readonly IGrainFactory grainFactory; + + public string Name + { + get { return "TextIndexer"; } + } + + public string EventsFilter + { + get { return "^content-"; } + } + + public GrainTextIndexer(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is ContentEvent contentEvent) + { + var index = grainFactory.GetGrain(contentEvent.SchemaId.Id); + + var id = contentEvent.ContentId; + + switch (@event.Payload) + { + case ContentDeleted _: + await index.DeleteAsync(id); + break; + case ContentCreated contentCreated: + await index.IndexAsync(Data(id, contentCreated.Data, true)); + break; + case ContentUpdateProposed contentUpdateProposed: + await index.IndexAsync(Data(id, contentUpdateProposed.Data, true)); + break; + case ContentUpdated contentUpdated: + await index.IndexAsync(Data(id, contentUpdated.Data, false)); + break; + case ContentChangesDiscarded _: + await index.CopyAsync(id, false); + break; + case ContentChangesPublished _: + case ContentStatusChanged contentStatusChanged when contentStatusChanged.Status == Status.Published: + await index.CopyAsync(id, true); + break; + } + } + } + + private static J Data(Guid contentId, NamedContentData data, bool onlySelf) + { + return new Update { Id = contentId, Data = data, OnlyDraft = onlySelf }; + } + + public async Task?> SearchAsync(string? queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published) + { + if (string.IsNullOrWhiteSpace(queryText)) + { + return null; + } + + var index = grainFactory.GetGrain(schemaId); + + using (Profiler.TraceMethod()) + { + var context = CreateContext(app, scope); + + return await index.SearchAsync(queryText, context); + } + } + + private static SearchContext CreateContext(IAppEntity app, Scope scope) + { + var languages = new HashSet(app.LanguagesConfig.Select(x => x.Key)); + + return new SearchContext { Languages = languages, Scope = scope }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs new file mode 100644 index 000000000..773dd1527 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public interface ITextIndexer + { + Task?> SearchAsync(string? queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs new file mode 100644 index 000000000..18d3956d4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Lucene.Net.Search; +using Lucene.Net.Util; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + internal sealed class IndexState + { + private const int NotFound = -1; + private const string MetaFor = "_fd"; + private readonly IndexSearcher? indexSearcher; + private readonly IndexWriter indexWriter; + private readonly BinaryDocValues binaryValues; + private readonly Dictionary<(Guid, byte), BytesRef> changes = new Dictionary<(Guid, byte), BytesRef>(); + private bool isClosed; + + public IndexState(IndexWriter indexWriter, IndexReader? indexReader = null, IndexSearcher? indexSearcher = null) + { + this.indexSearcher = indexSearcher; + this.indexWriter = indexWriter; + + if (indexReader != null) + { + binaryValues = MultiDocValues.GetBinaryValues(indexReader, MetaFor); + } + } + + public void Index(Guid id, byte draft, Document document, byte forDraft, byte forPublished) + { + var value = GetValue(forDraft, forPublished); + + document.SetBinaryDocValue(MetaFor, value); + + changes[(id, draft)] = value; + } + + public void Index(Guid id, byte draft, Term term, byte forDraft, byte forPublished) + { + var value = GetValue(forDraft, forPublished); + + indexWriter.UpdateBinaryDocValue(term, MetaFor, value); + + changes[(id, draft)] = value; + } + + public bool HasBeenAdded(Guid id, byte draft, Term term, out int docId) + { + docId = 0; + + if (changes.ContainsKey((id, draft))) + { + return true; + } + + if (indexSearcher != null && !isClosed) + { + var docs = indexSearcher.Search(new TermQuery(term), 1); + + docId = docs?.ScoreDocs.FirstOrDefault()?.Doc ?? NotFound; + + return docId > NotFound; + } + + return false; + } + + public bool TryGet(Guid id, byte draft, int docId, out byte forDraft, out byte forPublished) + { + forDraft = 0; + forPublished = 0; + + if (changes.TryGetValue((id, draft), out var forValue)) + { + forDraft = forValue.Bytes[0]; + forPublished = forValue.Bytes[1]; + + return true; + } + + if (!isClosed && docId != NotFound) + { + forValue = new BytesRef(); + + binaryValues?.Get(docId, forValue); + + if (forValue.Bytes.Length == 2) + { + forDraft = forValue.Bytes[0]; + forPublished = forValue.Bytes[1]; + + changes[(id, draft)] = forValue; + + return true; + } + } + + return false; + } + + public bool TryGet(int docId, out byte forDraft, out byte forPublished) + { + forDraft = 0; + forPublished = 0; + + if (!isClosed && docId != NotFound) + { + var forValue = new BytesRef(); + + binaryValues?.Get(docId, forValue); + + if (forValue.Bytes.Length == 2) + { + forDraft = forValue.Bytes[0]; + forPublished = forValue.Bytes[1]; + + return true; + } + } + + return false; + } + + private static BytesRef GetValue(byte forDraft, byte forPublished) + { + return new BytesRef(new[] { forDraft, forPublished }); + } + + public void CloseReader() + { + isClosed = true; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs new file mode 100644 index 000000000..80a98fbb9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Util; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class MultiLanguageAnalyzer : AnalyzerWrapper + { + private readonly StandardAnalyzer fallbackAnalyzer; + private readonly Dictionary analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public MultiLanguageAnalyzer(LuceneVersion version) + : base(PER_FIELD_REUSE_STRATEGY) + { + fallbackAnalyzer = new StandardAnalyzer(version); + + foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) + { + if (typeof(Analyzer).IsAssignableFrom(type)) + { + var language = type.Namespace!.Split('.').Last(); + + if (language.Length == 2) + { + try + { + var analyzer = Activator.CreateInstance(type, version)!; + + analyzers[language] = (Analyzer)analyzer; + } + catch (MissingMethodException) + { + continue; + } + } + } + } + } + + protected override Analyzer GetWrappedAnalyzer(string fieldName) + { + if (fieldName.Length > 0) + { + var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer; + + return analyzer; + } + else + { + return fallbackAnalyzer; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Scope.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs new file mode 100644 index 000000000..83f701b7c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs @@ -0,0 +1,213 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + internal sealed class TextIndexContent + { + private const string MetaId = "_id"; + private const string MetaKey = "_key"; + private readonly IndexWriter indexWriter; + private readonly IndexState indexState; + private readonly Guid id; + + public TextIndexContent(IndexWriter indexWriter, IndexState indexState, Guid id) + { + this.indexWriter = indexWriter; + this.indexState = indexState; + + this.id = id; + } + + public void Delete() + { + indexWriter.DeleteDocuments(new Term(MetaId, id.ToString())); + } + + public static bool TryGetId(int docId, Scope scope, IndexReader reader, IndexState indexState, out Guid result) + { + result = Guid.Empty; + + if (!indexState.TryGet(docId, out var draft, out var published)) + { + return false; + } + + if (scope == Scope.Draft && draft != 1) + { + return false; + } + + if (scope == Scope.Published && published != 1) + { + return false; + } + + var document = reader.Document(docId); + + var idString = document.Get(MetaId); + + if (!Guid.TryParse(idString, out result)) + { + return false; + } + + return true; + } + + public void Index(NamedContentData data, bool onlyDraft) + { + var converted = CreateDocument(data); + + Upsert(converted, 1, 1, 0); + + var isPublishDocumentAdded = IsAdded(0, out var docId); + var isPublishForPublished = IsForPublished(0, docId); + + if (!onlyDraft && isPublishDocumentAdded && isPublishForPublished) + { + Upsert(converted, 0, 0, 1); + } + else if (!onlyDraft || !isPublishDocumentAdded) + { + Upsert(converted, 0, 0, 0); + } + else + { + UpdateFor(0, 0, isPublishForPublished ? (byte)1 : (byte)0); + } + } + + public void Copy(bool fromDraft) + { + if (fromDraft) + { + UpdateFor(1, 1, 0); + UpdateFor(0, 0, 1); + } + else + { + UpdateFor(1, 0, 0); + UpdateFor(0, 1, 1); + } + } + + private static Document CreateDocument(NamedContentData data) + { + var languages = new Dictionary(); + + void AppendText(string language, string text) + { + if (!string.IsNullOrWhiteSpace(text)) + { + var sb = languages.GetOrAddNew(language); + + if (sb.Length > 0) + { + sb.Append(" "); + } + + sb.Append(text); + } + } + + foreach (var field in data) + { + if (field.Value != null) + { + foreach (var fieldValue in field.Value) + { + var appendText = new Action(text => AppendText(fieldValue.Key, text)); + + AppendJsonText(fieldValue.Value, appendText); + } + } + } + + var document = new Document(); + + foreach (var field in languages) + { + document.AddTextField(field.Key, field.Value.ToString(), Field.Store.NO); + } + + return document; + } + + private void UpdateFor(byte draft, byte forDraft, byte forPublished) + { + var term = new Term(MetaKey, BuildKey(draft)); + + indexState.Index(id, draft, term, forDraft, forPublished); + } + + private void Upsert(Document document, byte draft, byte forDraft, byte forPublished) + { + if (document != null) + { + document.RemoveField(MetaId); + document.RemoveField(MetaKey); + + var contentId = id.ToString(); + var contentKey = BuildKey(draft); + + document.AddStringField(MetaId, contentId, Field.Store.YES); + document.AddStringField(MetaKey, contentKey, Field.Store.YES); + + indexState.Index(id, draft, document, forDraft, forPublished); + + indexWriter.UpdateDocument(new Term(MetaKey, contentKey), document); + } + } + + private static void AppendJsonText(IJsonValue value, Action appendText) + { + if (value.Type == JsonValueType.String) + { + appendText(value.ToString()); + } + else if (value is JsonArray array) + { + foreach (var item in array) + { + AppendJsonText(item, appendText); + } + } + else if (value is JsonObject obj) + { + foreach (var item in obj.Values) + { + AppendJsonText(item, appendText); + } + } + } + + private bool IsAdded(byte draft, out int docId) + { + return indexState.HasBeenAdded(id, draft, new Term(MetaKey, BuildKey(draft)), out docId); + } + + private bool IsForPublished(byte draft, int docId) + { + return indexState.TryGet(id, draft, docId, out _, out var p) && p == 1; + } + + private string BuildKey(byte draft) + { + return $"{id}_{draft}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs new file mode 100644 index 000000000..6730a6203 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs @@ -0,0 +1,271 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Lucene.Net.Analysis; +using Lucene.Net.Index; +using Lucene.Net.QueryParsers.Classic; +using Lucene.Net.Search; +using Lucene.Net.Store; +using Lucene.Net.Util; +using Squidex.Domain.Apps.Core; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain + { + private const LuceneVersion Version = LuceneVersion.LUCENE_48; + private const int MaxResults = 2000; + private const int MaxUpdates = 400; + private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(10); + private static readonly MergeScheduler MergeScheduler = new ConcurrentMergeScheduler(); + private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version); + private static readonly string[] Invariant = { InvariantPartitioning.Key }; + private readonly SnapshotDeletionPolicy snapshotter = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + private readonly IAssetStore assetStore; + private IDisposable? timer; + private DirectoryInfo directory; + private IndexWriter? indexWriter; + private IndexReader? indexReader; + private IndexSearcher? indexSearcher; + private IndexState? indexState; + private QueryParser? queryParser; + private HashSet? currentLanguages; + private int updates; + + public TextIndexerGrain(IAssetStore assetStore) + { + Guard.NotNull(assetStore); + + this.assetStore = assetStore; + } + + public override async Task OnDeactivateAsync() + { + await DeactivateAsync(true); + } + + protected override async Task OnActivateAsync(Guid key) + { + directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}")); + + await assetStore.DownloadAsync(directory); + + var config = new IndexWriterConfig(Version, Analyzer) + { + IndexDeletionPolicy = snapshotter, + MergePolicy = new TieredMergePolicy(), + MergeScheduler = MergeScheduler + }; + + indexWriter = new IndexWriter(FSDirectory.Open(directory), config); + + if (indexWriter.NumDocs > 0) + { + OpenReader(); + } + else + { + indexState = new IndexState(indexWriter); + } + } + + public Task IndexAsync(J update) + { + return IndexInternalAsync(update); + } + + private Task IndexInternalAsync(Update update) + { + if (indexWriter != null && indexState != null) + { + var content = new TextIndexContent(indexWriter, indexState, update.Id); + + content.Index(update.Data, update.OnlyDraft); + } + + return TryFlushAsync(); + } + + public Task CopyAsync(Guid id, bool fromDraft) + { + if (indexWriter != null && indexState != null) + { + var content = new TextIndexContent(indexWriter, indexState, id); + + content.Copy(fromDraft); + } + + return TryFlushAsync(); + } + + public Task DeleteAsync(Guid id) + { + if (indexWriter != null && indexState != null) + { + var content = new TextIndexContent(indexWriter, indexState, id); + + content.Delete(); + } + + return TryFlushAsync(); + } + + public Task> SearchAsync(string queryText, SearchContext context) + { + var result = new List(); + + if (!string.IsNullOrWhiteSpace(queryText)) + { + var query = BuildQuery(queryText, context); + + if (indexReader == null && indexWriter?.NumDocs > 0) + { + OpenReader(); + } + + if (indexReader != null && indexSearcher != null && indexState != null) + { + var found = new HashSet(); + + var hits = indexSearcher.Search(query, MaxResults).ScoreDocs; + + foreach (var hit in hits) + { + if (TextIndexContent.TryGetId(hit.Doc, context.Scope, indexReader, indexState, out var id)) + { + if (found.Add(id)) + { + result.Add(id); + } + } + } + } + } + + return Task.FromResult(result.ToList()); + } + + private Query BuildQuery(string query, SearchContext context) + { + if (queryParser == null || currentLanguages == null || !currentLanguages.SetEquals(context.Languages)) + { + var fields = context.Languages.Union(Invariant).ToArray(); + + queryParser = new MultiFieldQueryParser(Version, fields, Analyzer); + + currentLanguages = context.Languages; + } + + try + { + return queryParser.Parse(query); + } + catch (ParseException ex) + { + throw new ValidationException(ex.Message); + } + } + + private async Task TryFlushAsync() + { + timer?.Dispose(); + + updates++; + + if (updates >= MaxUpdates) + { + await FlushAsync(); + + return true; + } + else + { + CleanReader(); + + try + { + timer = RegisterTimer(_ => FlushAsync(), null, CommitDelay, CommitDelay); + } + catch (InvalidOperationException) + { + return false; + } + } + + return false; + } + + public async Task FlushAsync() + { + if (updates > 0 && indexWriter != null) + { + indexWriter.Commit(); + indexWriter.Flush(true, true); + + CleanReader(); + + var commit = snapshotter.Snapshot(); + try + { + await assetStore.UploadDirectoryAsync(directory, commit); + } + finally + { + snapshotter.Release(commit); + } + + updates = 0; + } + } + + public async Task DeactivateAsync(bool deleteFolder = false) + { + await FlushAsync(); + + CleanWriter(); + CleanReader(); + + if (deleteFolder && directory.Exists) + { + directory.Delete(true); + } + } + + private void OpenReader() + { + if (indexWriter != null) + { + indexReader = indexWriter!.GetReader(true); + indexSearcher = new IndexSearcher(indexReader); + indexState = new IndexState(indexWriter, indexReader, indexSearcher); + } + } + + private void CleanReader() + { + indexReader?.Dispose(); + indexReader = null; + indexSearcher = null; + indexState?.CloseReader(); + } + + private void CleanWriter() + { + indexWriter?.Dispose(); + indexWriter = null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Update.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs new file mode 100644 index 000000000..903868e14 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class Context + { + public IDictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public IAppEntity App { get; set; } + + public ClaimsPrincipal User { get; } + + public ClaimsPermissions Permissions { get; private set; } = ClaimsPermissions.Empty; + + public bool IsFrontendClient { get; private set; } + + public Context(ClaimsPrincipal user) + { + Guard.NotNull(user); + + User = user; + + UpdatePermissions(); + } + + public Context(ClaimsPrincipal user, IAppEntity app) + : this(user) + { + App = app; + } + + public static Context Anonymous() + { + return new Context(new ClaimsPrincipal()); + } + + public void UpdatePermissions() + { + Permissions = User.Permissions(); + + IsFrontendClient = User.IsInClient(DefaultClients.Frontend); + } + + public Context Clone() + { + var clone = new Context(User, App); + + foreach (var kvp in Headers) + { + clone.Headers[kvp.Key] = kvp.Value; + } + + return clone; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/DomainObjectState.cs rename to backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs diff --git a/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/EntityExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs new file mode 100644 index 000000000..9e13f4f36 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities +{ + public static class EntityMapper + { + public static T Update(this T entity, Envelope envelope, Action? updater = null) where T : IEntity + { + var @event = (SquidexEvent)envelope.Payload; + + var headers = envelope.Headers; + + SetId(entity, headers); + SetCreated(entity, headers); + SetCreatedBy(entity, @event); + SetLastModified(entity, headers); + SetLastModifiedBy(entity, @event); + SetVersion(entity, headers); + + updater?.Invoke(@event, entity); + + return entity; + } + + private static void SetId(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty) + { + updateable.Id = headers.AggregateId(); + } + } + + private static void SetVersion(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntityWithVersion updateable) + { + updateable.Version = headers.EventStreamNumber(); + } + } + + private static void SetCreated(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntity updateable && updateable.Created == default) + { + updateable.Created = headers.Timestamp(); + } + } + + private static void SetCreatedBy(IEntity entity, SquidexEvent @event) + { + if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) + { + withCreatedBy.CreatedBy = @event.Actor; + } + } + + private static void SetLastModified(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntity updateable) + { + updateable.LastModified = headers.Timestamp(); + } + } + + private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event) + { + if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) + { + withModifiedBy.LastModifiedBy = @event.Actor; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs new file mode 100644 index 000000000..a354c4a2b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryEvent + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid AppId { get; set; } + + public Instant Created { get; set; } + + public RefToken Actor { get; set; } + + public long Version { get; set; } + + public string Channel { get; set; } + + public string Message { get; set; } + + public Dictionary Parameters { get; set; } = new Dictionary(); + + public HistoryEvent() + { + } + + public HistoryEvent(string channel, string message) + { + Guard.NotNullOrEmpty(channel); + Guard.NotNullOrEmpty(message); + + Channel = channel; + + Message = message; + } + + public HistoryEvent Param(string key, object? value) + { + if (value != null) + { + var formatted = value.ToString(); + + if (formatted != null) + { + Parameters[key] = formatted; + } + } + + return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs new file mode 100644 index 000000000..5b85a97fe --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.History +{ + public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator + { + private readonly Dictionary texts = new Dictionary(); + private readonly TypeNameRegistry typeNameRegistry; + + public IReadOnlyDictionary Texts + { + get { return texts; } + } + + protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry); + + this.typeNameRegistry = typeNameRegistry; + } + + protected void AddEventMessage(string message) where TEvent : IEvent + { + Guard.NotNullOrEmpty(message); + + texts[typeNameRegistry.GetName()] = message; + } + + protected bool HasEventText(IEvent @event) + { + var message = typeNameRegistry.GetName(@event.GetType()); + + return texts.ContainsKey(message); + } + + protected HistoryEvent ForEvent(IEvent @event, string channel) + { + var message = typeNameRegistry.GetName(@event.GetType()); + + return new HistoryEvent(channel, message); + } + + public Task CreateEventAsync(Envelope @event) + { + if (HasEventText(@event.Payload)) + { + return CreateEventCoreAsync(@event); + } + + return Task.FromResult(null); + } + + protected abstract Task CreateEventCoreAsync(Envelope @event); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs new file mode 100644 index 000000000..426f8d450 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryService : IHistoryService, IEventConsumer + { + private readonly Dictionary texts = new Dictionary(); + private readonly List creators; + private readonly IHistoryEventRepository repository; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public HistoryService(IHistoryEventRepository repository, IEnumerable creators) + { + Guard.NotNull(repository); + Guard.NotNull(creators); + + this.creators = creators.ToList(); + + foreach (var creator in this.creators) + { + foreach (var text in creator.Texts) + { + texts[text.Key] = text.Value; + } + } + + this.repository = repository; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return repository.ClearAsync(); + } + + public async Task On(Envelope @event) + { + foreach (var creator in creators) + { + var historyEvent = await creator.CreateEventAsync(@event); + + if (historyEvent != null) + { + var appEvent = (AppEvent)@event.Payload; + + historyEvent.Actor = appEvent.Actor; + historyEvent.AppId = appEvent.AppId.Id; + historyEvent.Created = @event.Headers.Timestamp(); + historyEvent.Version = @event.Headers.EventStreamNumber(); + + await repository.InsertAsync(historyEvent); + } + } + } + + public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) + { + var items = await repository.QueryByChannelAsync(appId, channelPrefix, count); + + return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs new file mode 100644 index 000000000..6e7a097f1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public interface IHistoryEventsCreator + { + IReadOnlyDictionary Texts { get; } + + Task CreateEventAsync(Envelope @event); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Notifications/INotificationEmailSender.cs diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NoopNotificationEmailSender.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs new file mode 100644 index 000000000..a521530a6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs @@ -0,0 +1,121 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.History.Notifications +{ + public sealed class NotificationEmailEventConsumer : IEventConsumer + { + private static readonly Duration MaxAge = Duration.FromDays(2); + private readonly INotificationEmailSender emailSender; + private readonly IUserResolver userResolver; + private readonly ISemanticLog log; + + public string Name + { + get { return "NotificationEmailSender"; } + } + + public string EventsFilter + { + get { return "^app-"; } + } + + public NotificationEmailEventConsumer(INotificationEmailSender emailSender, IUserResolver userResolver, ISemanticLog log) + { + Guard.NotNull(emailSender); + Guard.NotNull(userResolver); + Guard.NotNull(log); + + this.emailSender = emailSender; + this.userResolver = userResolver; + + this.log = log; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (!emailSender.IsActive) + { + return; + } + + if (@event.Headers.EventStreamNumber() <= 1) + { + return; + } + + var now = SystemClock.Instance.GetCurrentInstant(); + + var timestamp = @event.Headers.Timestamp(); + + if (now - timestamp > MaxAge) + { + return; + } + + if (@event.Payload is AppContributorAssigned appContributorAssigned) + { + if (!appContributorAssigned.Actor.IsSubject || !appContributorAssigned.IsAdded) + { + return; + } + + var assignerId = appContributorAssigned.Actor.Identifier; + var assigneeId = appContributorAssigned.ContributorId; + + var assigner = await userResolver.FindByIdOrEmailAsync(assignerId); + + if (assigner == null) + { + LogWarning($"Assigner {assignerId} not found"); + return; + } + + var assignee = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.ContributorId); + + if (assignee == null) + { + LogWarning($"Assignee {assigneeId} not found"); + return; + } + + var appName = appContributorAssigned.AppId.Name; + + var isCreated = appContributorAssigned.IsCreated; + + await emailSender.SendContributorEmailAsync(assigner, assignee, appName, isCreated); + } + } + + private void LogWarning(string reason) + { + log.LogWarning(w => w + .WriteProperty("action", "InviteUser") + .WriteProperty("status", "Failed") + .WriteProperty("reason", reason)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs new file mode 100644 index 000000000..f76c6087a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Email; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.History.Notifications +{ + public sealed class NotificationEmailSender : INotificationEmailSender + { + private readonly IEmailSender emailSender; + private readonly IEmailUrlGenerator emailUrlGenerator; + private readonly ISemanticLog log; + private readonly NotificationEmailTextOptions texts; + + public bool IsActive + { + get { return true; } + } + + public NotificationEmailSender( + IOptions texts, + IEmailSender emailSender, + IEmailUrlGenerator emailUrlGenerator, + ISemanticLog log) + { + Guard.NotNull(texts); + Guard.NotNull(emailSender); + Guard.NotNull(emailUrlGenerator); + Guard.NotNull(log); + + this.texts = texts.Value; + this.emailSender = emailSender; + this.emailUrlGenerator = emailUrlGenerator; + this.log = log; + } + + public Task SendContributorEmailAsync(IUser assigner, IUser assignee, string appName, bool isCreated) + { + Guard.NotNull(assigner); + Guard.NotNull(assignee); + Guard.NotNull(appName); + + if (assignee.HasConsent()) + { + return SendEmailAsync(texts.ExistingUserSubject, texts.ExistingUserBody, assigner, assignee, appName); + } + else + { + return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); + } + } + + private async Task SendEmailAsync(string emailSubj, string emailBody, IUser assigner, IUser assignee, string appName) + { + if (string.IsNullOrWhiteSpace(emailBody)) + { + LogWarning("No email subject configured for new users"); + return; + } + + if (string.IsNullOrWhiteSpace(emailSubj)) + { + LogWarning("No email body configured for new users"); + return; + } + + var appUrl = emailUrlGenerator.GenerateUIUrl(); + + emailSubj = Format(emailSubj, assigner, assignee, appUrl, appName); + emailBody = Format(emailBody, assigner, assignee, appUrl, appName); + + await emailSender.SendAsync(assignee.Email, emailSubj, emailBody); + } + + private void LogWarning(string reason) + { + log.LogWarning(w => w + .WriteProperty("action", "InviteUser") + .WriteProperty("status", "Failed") + .WriteProperty("reason", reason)); + } + + private static string Format(string text, IUser assigner, IUser assignee, string uiUrl, string appName) + { + text = text.Replace("$APP_NAME", appName); + + if (assigner != null) + { + text = text.Replace("$ASSIGNER_EMAIL", assigner.Email); + text = text.Replace("$ASSIGNER_NAME", assigner.DisplayName()); + } + + if (assignee != null) + { + text = text.Replace("$ASSIGNEE_EMAIL", assignee.Email); + text = text.Replace("$ASSIGNEE_NAME", assignee.DisplayName()); + } + + text = text.Replace("$UI_URL", uiUrl); + + return text; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailTextOptions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs new file mode 100644 index 000000000..0d7b503c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.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 NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class ParsedHistoryEvent + { + private readonly HistoryEvent item; + private readonly Lazy message; + + public Guid Id + { + get { return item.Id; } + } + + public Instant Created + { + get { return item.Created; } + } + + public RefToken Actor + { + get { return item.Actor; } + } + + public long Version + { + get { return item.Version; } + } + + public string Channel + { + get { return item.Channel; } + } + + public string? Message + { + get { return message.Value; } + } + + public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary texts) + { + this.item = item; + + message = new Lazy(() => + { + if (texts.TryGetValue(item.Message, out var result)) + { + foreach (var kvp in item.Parameters) + { + result = result.Replace("[" + kvp.Key + "]", kvp.Value); + } + + return result; + } + + return null; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs rename to backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs diff --git a/src/Squidex.Domain.Apps.Entities/IAppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IAppCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs new file mode 100644 index 000000000..bc4dd03ff --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IAppProvider + { + Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(Guid appId, Guid id); + + Task GetAppAsync(Guid appId); + + Task GetAppAsync(string appName); + + Task> GetUserAppsAsync(string userId, PermissionSet permissions); + + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); + + Task GetSchemaAsync(Guid appId, string name); + + Task> GetSchemasAsync(Guid appId); + + Task> GetRulesAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IContextProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IContextProvider.cs rename to backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntity.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs new file mode 100644 index 000000000..516aead56 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithCacheDependencies + { + HashSet CacheDependencies { get; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs rename to backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs diff --git a/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs rename to backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs new file mode 100644 index 000000000..863e14796 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class Q : Cloneable + { + public static readonly Q Empty = new Q(); + + public IReadOnlyList Ids { get; private set; } + + public string? ODataQuery { get; private set; } + + public string? JsonQuery { get; private set; } + + public Q WithODataQuery(string? odataQuery) + { + return Clone(c => c.ODataQuery = odataQuery); + } + + public Q WithJsonQuery(string? jsonQuery) + { + return Clone(c => c.JsonQuery = jsonQuery); + } + + public Q WithIds(params Guid[] ids) + { + return Clone(c => c.Ids = ids.ToList()); + } + + public Q WithIds(IEnumerable ids) + { + return Clone(c => c.Ids = ids.ToList()); + } + + public Q WithIds(string? ids) + { + if (!string.IsNullOrEmpty(ids)) + { + return Clone(c => + { + var idsList = new List(); + + foreach (var id in ids.Split(',')) + { + if (Guid.TryParse(id, out var guid)) + { + idsList.Add(guid); + } + } + + c.Ids = idsList; + }); + } + + return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs new file mode 100644 index 000000000..01226e61d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class BackupRules : BackupHandler + { + private readonly HashSet ruleIds = new HashSet(); + private readonly IRulesIndex indexForRules; + + public override string Name { get; } = "Rules"; + + public BackupRules(IRulesIndex indexForRules) + { + Guard.NotNull(indexForRules); + + this.indexForRules = indexForRules; + } + + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case RuleCreated ruleCreated: + ruleIds.Add(ruleCreated.RuleId); + break; + case RuleDeleted ruleDeleted: + ruleIds.Remove(ruleDeleted.RuleId); + break; + } + + return TaskHelper.True; + } + + public override Task RestoreAsync(Guid appId, BackupReader reader) + { + return indexForRules.RebuildAsync(appId, ruleIds); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs new file mode 100644 index 000000000..2f8a80964 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public static class GuardRule + { + public static Task CanCreate(CreateRule command, IAppProvider appProvider) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot create rule.", async e => + { + if (command.Trigger == null) + { + e(Not.Defined("Trigger"), nameof(command.Trigger)); + } + else + { + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); + + errors.Foreach(x => x.AddTo(e)); + } + + if (command.Action == null) + { + e(Not.Defined("Action"), nameof(command.Action)); + } + else + { + var errors = command.Action.Validate(); + + errors.Foreach(x => x.AddTo(e)); + } + }); + } + + public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider, Rule rule) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot update rule.", async e => + { + if (command.Trigger == null && command.Action == null && command.Name == null) + { + e(Not.Defined("Either trigger, action or name"), nameof(command.Trigger), nameof(command.Action)); + } + + if (command.Trigger != null) + { + var errors = await RuleTriggerValidator.ValidateAsync(appId, command.Trigger, appProvider); + + errors.Foreach(x => x.AddTo(e)); + } + + if (command.Action != null) + { + var errors = command.Action.Validate(); + + errors.Foreach(x => x.AddTo(e)); + } + + if (command.Name != null && string.Equals(rule.Name, command.Name)) + { + e(Not.New("Rule", "name"), nameof(command.Name)); + } + }); + } + + public static void CanEnable(EnableRule command, Rule rule) + { + Guard.NotNull(command); + + if (rule.IsEnabled) + { + throw new DomainException("Rule is already enabled."); + } + } + + public static void CanDisable(DisableRule command, Rule rule) + { + Guard.NotNull(command); + + if (!rule.IsEnabled) + { + throw new DomainException("Rule is already disabled."); + } + } + + public static void CanDelete(DeleteRule command) + { + Guard.NotNull(command); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs new file mode 100644 index 000000000..64c4a1301 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> + { + public Func> SchemaProvider { get; } + + public RuleTriggerValidator(Func> schemaProvider) + { + SchemaProvider = schemaProvider; + } + + public static Task> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider) + { + Guard.NotNull(action); + Guard.NotNull(appProvider); + + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); + + return action.Accept(visitor); + } + + public Task> Visit(AssetChangedTriggerV2 trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task> Visit(ManualTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task> Visit(SchemaChangedTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task> Visit(UsageTrigger trigger) + { + var errors = new List(); + + if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) + { + errors.Add(new ValidationError(Not.Between("Num days", 1, 30), nameof(trigger.NumDays))); + } + + return Task.FromResult>(errors); + } + + public async Task> Visit(ContentChangedTriggerV2 trigger) + { + var errors = new List(); + + if (trigger.Schemas != null) + { + var tasks = new List>(); + + foreach (var schema in trigger.Schemas) + { + if (schema.SchemaId == Guid.Empty) + { + errors.Add(new ValidationError(Not.Defined("Schema id"), nameof(trigger.Schemas))); + } + else + { + tasks.Add(CheckSchemaAsync(schema)); + } + } + + var checkErrors = await Task.WhenAll(tasks); + + errors.AddRange(checkErrors.Where(x => x != null)); + } + + return errors; + } + + private async Task CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) + { + if (await SchemaProvider(schema.SchemaId) == null) + { + return new ValidationError($"Schema {schema.SchemaId} does not exist.", nameof(ContentChangedTriggerV2.Schemas)); + } + + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs new file mode 100644 index 000000000..c45443510 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public interface IRuleEventEntity : IEntity + { + RuleJob Job { get; } + + Instant? NextAttempt { get; } + + RuleJobResult JobResult { get; } + + RuleResult Result { get; } + + int NumCalls { get; } + + string? LastDump { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs new file mode 100644 index 000000000..c3a346594 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesIndex : ICommandMiddleware, IRulesIndex + { + private readonly IGrainFactory grainFactory; + + public RulesIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public Task RebuildAsync(Guid appId, HashSet rues) + { + return Index(appId).RebuildAsync(rues); + } + + public async Task> GetRulesAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + var ids = await GetRuleIdsAsync(appId); + + var rules = + await Task.WhenAll( + ids.Select(GetRuleAsync)); + + return rules.Where(x => x != null).ToList(); + } + } + + private async Task GetRuleAsync(Guid id) + { + using (Profiler.TraceMethod()) + { + var ruleEntity = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(ruleEntity.Value)) + { + return ruleEntity.Value; + } + + return null; + } + } + + private async Task> GetRuleIdsAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdsAsync(); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + await next(); + + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateRule createRule: + await CreateRuleAsync(createRule); + break; + case DeleteRule deleteRule: + await DeleteRuleAsync(deleteRule); + break; + } + } + } + + private async Task CreateRuleAsync(CreateRule command) + { + await Index(command.AppId.Id).AddAsync(command.RuleId); + } + + private async Task DeleteRuleAsync(DeleteRule command) + { + var id = command.RuleId; + + var rule = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(rule.Value)) + { + await Index(rule.Value.AppId.Id).RemoveAsync(id); + } + } + + private IRulesByAppIndexGrain Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + + private static bool IsFound(IRuleEntity rule) + { + return rule.Version > EtagVersion.Empty && !rule.IsDeleted; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs new file mode 100644 index 000000000..53325adb2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class ManualTriggerHandler : RuleTriggerHandler + { + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedManualEvent + { + Name = "Manual" + }; + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedManualEvent @event, ManualTrigger trigger) + { + return true; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs new file mode 100644 index 000000000..a5ef426f2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Rules.Queries +{ + public sealed class RuleEnricher : IRuleEnricher + { + private readonly IRuleEventRepository ruleEventRepository; + + public RuleEnricher(IRuleEventRepository ruleEventRepository) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + + this.ruleEventRepository = ruleEventRepository; + } + + public async Task EnrichAsync(IRuleEntity rule, Context context) + { + Guard.NotNull(rule, nameof(rule)); + + var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable rules, Context context) + { + Guard.NotNull(rules, nameof(rules)); + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var results = new List(); + + foreach (var rule in rules) + { + var result = SimpleMapper.Map(rule, new RuleEntity()); + + results.Add(result); + } + + foreach (var group in results.GroupBy(x => x.AppId.Id)) + { + var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key); + + foreach (var rule in group) + { + var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); + + if (statistic != null) + { + rule.LastExecuted = statistic.LastExecuted; + rule.NumFailed = statistic.NumFailed; + rule.NumSucceeded = statistic.NumSucceeded; + + rule.CacheDependencies = new HashSet + { + statistic.LastExecuted + }; + } + } + } + + return results; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs new file mode 100644 index 000000000..d24502f06 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.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 System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules.Repositories +{ + public interface IRuleEventRepository + { + Task EnqueueAsync(RuleJob job, Instant nextAttempt); + + Task EnqueueAsync(Guid id, Instant nextAttempt); + + Task CancelAsync(Guid id); + + Task MarkSentAsync(RuleJob job, string? dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall); + + Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default); + + Task CountByAppAsync(Guid appId); + + Task> QueryStatisticsByAppAsync(Guid appId); + + Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20); + + Task FindAsync(Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs new file mode 100644 index 000000000..eb1841bea --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs @@ -0,0 +1,163 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using NodaTime; +using Orleans; +using Orleans.Runtime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleDequeuerGrain : Grain, IRuleDequeuerGrain, IRemindable + { + private readonly ITargetBlock requestBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly RuleService ruleService; + private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); + private readonly IClock clock; + private readonly ISemanticLog log; + + public RuleDequeuerGrain(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) + { + Guard.NotNull(ruleEventRepository); + Guard.NotNull(ruleService); + Guard.NotNull(clock); + Guard.NotNull(log); + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + + this.clock = clock; + + this.log = log; + + requestBlock = + new PartitionedActionBlock(HandleAsync, x => x.Job.ExecutionPartition, + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); + } + + public override Task OnActivateAsync() + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => QueryAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); + } + + public override Task OnDeactivateAsync() + { + requestBlock.Complete(); + + return requestBlock.Completion; + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public async Task QueryAsync() + { + try + { + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "QueueWebhookEvents") + .WriteProperty("status", "Failed")); + } + } + + public async Task HandleAsync(IRuleEventEntity @event) + { + if (!executing.TryAdd(@event.Id, false)) + { + return; + } + + try + { + var job = @event.Job; + + var (response, elapsed) = await ruleService.InvokeAsync(job.ActionName, job.ActionData); + + var jobInvoke = ComputeJobInvoke(response.Status, @event, job); + var jobResult = ComputeJobResult(response.Status, jobInvoke); + + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.MarkSentAsync(@event.Job, response.Dump, response.Status, jobResult, elapsed, now, jobInvoke); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "SendWebhookEvent") + .WriteProperty("status", "Failed")); + } + finally + { + executing.TryRemove(@event.Id, out _); + } + } + + private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) + { + if (result != RuleResult.Success && !nextCall.HasValue) + { + return RuleJobResult.Failed; + } + else if (result != RuleResult.Success && nextCall.HasValue) + { + return RuleJobResult.Retry; + } + else + { + return RuleJobResult.Success; + } + } + + private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) + { + if (result != RuleResult.Success) + { + switch (@event.NumCalls) + { + case 0: + return job.Created.Plus(Duration.FromMinutes(5)); + case 1: + return job.Created.Plus(Duration.FromHours(1)); + case 2: + return job.Created.Plus(Duration.FromHours(6)); + case 3: + return job.Created.Plus(Duration.FromHours(12)); + } + } + + return null; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs new file mode 100644 index 000000000..593685f40 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10); + private readonly IRuleEventRepository ruleEventRepository; + private readonly IAppProvider appProvider; + private readonly IMemoryCache cache; + private readonly RuleService ruleService; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, IRuleEventRepository ruleEventRepository, + RuleService ruleService) + { + Guard.NotNull(appProvider); + Guard.NotNull(cache); + Guard.NotNull(ruleEventRepository); + Guard.NotNull(ruleService); + + this.appProvider = appProvider; + + this.cache = cache; + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task Enqueue(Rule rule, Guid ruleId, Envelope @event) + { + Guard.NotNull(rule, nameof(rule)); + Guard.NotNull(@event, nameof(@event)); + + var job = await ruleService.CreateJobAsync(rule, ruleId, @event); + + if (job != null) + { + await ruleEventRepository.EnqueueAsync(job, job.Created); + } + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + var rules = await GetRulesAsync(appEvent.AppId.Id); + + foreach (var ruleEntity in rules) + { + await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event); + } + } + } + + private Task> GetRulesAsync(Guid appId) + { + return cache.GetOrCreateAsync(appId, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return appProvider.GetRulesAsync(appId); + }); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs new file mode 100644 index 000000000..8c131db24 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleEntity : IEnrichedRuleEntity + { + public Guid Id { get; set; } + + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + + public long Version { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public Rule RuleDef { get; set; } + + public bool IsDeleted { get; set; } + + public int NumSucceeded { get; set; } + + public int NumFailed { get; set; } + + public Instant? LastExecuted { get; set; } + + public HashSet CacheDependencies { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs new file mode 100644 index 000000000..02e614bff --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -0,0 +1,154 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Guards; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleGrain : DomainObjectGrain, IRuleGrain + { + private readonly IAppProvider appProvider; + private readonly IRuleEnqueuer ruleEnqueuer; + + public RuleGrain(IStore store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) + : base(store, log) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer)); + + this.appProvider = appProvider; + + this.ruleEnqueuer = ruleEnqueuer; + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateRule createRule: + return CreateReturnAsync(createRule, async c => + { + await GuardRule.CanCreate(c, appProvider); + + Create(c); + + return Snapshot; + }); + case UpdateRule updateRule: + return UpdateReturnAsync(updateRule, async c => + { + await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider, Snapshot.RuleDef); + + Update(c); + + return Snapshot; + }); + case EnableRule enableRule: + return UpdateReturn(enableRule, c => + { + GuardRule.CanEnable(c, Snapshot.RuleDef); + + Enable(c); + + return Snapshot; + }); + case DisableRule disableRule: + return UpdateReturn(disableRule, c => + { + GuardRule.CanDisable(c, Snapshot.RuleDef); + + Disable(c); + + return Snapshot; + }); + case DeleteRule deleteRule: + return Update(deleteRule, c => + { + GuardRule.CanDelete(deleteRule); + + Delete(c); + }); + case TriggerRule triggerRule: + return Trigger(triggerRule); + default: + throw new NotSupportedException(); + } + } + + private async Task Trigger(TriggerRule command) + { + var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId }); + + await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); + + return null; + } + + public void Create(CreateRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + } + + public void Update(UpdateRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + } + + public void Enable(EnableRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + } + + public void Disable(DisableRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + } + + public void Delete(DeleteRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Rule has already been deleted."); + } + } + + public Task> GetStateAsync() + { + return J.AsTask(Snapshot); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs new file mode 100644 index 000000000..b96cec588 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware + { + private readonly IUsageTrackerGrain usageTrackerGrain; + + public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + switch (context.Command) + { + case DeleteRule deleteRule: + await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); + break; + case CreateRule createRule: + { + if (createRule.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); + } + + break; + } + + case UpdateRule ruleUpdated: + { + if (ruleUpdated.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); + } + + break; + } + } + + await next(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs new file mode 100644 index 000000000..16dccf158 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Orleans.Runtime; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + [Reentrant] + public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain + { + private readonly IGrainState state; + private readonly IUsageTracker usageTracker; + + public sealed class Target + { + public NamedId AppId { get; set; } + + public int Limits { get; set; } + + public int? NumDays { get; set; } + + public DateTime? Triggered { get; set; } + } + + [CollectionName("UsageTracker")] + public sealed class GrainState + { + public Dictionary Targets { get; set; } = new Dictionary(); + } + + public UsageTrackerGrain(IGrainState state, IUsageTracker usageTracker) + { + Guard.NotNull(state); + Guard.NotNull(usageTracker); + + this.state = state; + + this.usageTracker = usageTracker; + } + + protected override Task OnActivateAsync(string key) + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); + + return TaskHelper.Done; + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + public async Task CheckUsagesAsync() + { + var today = DateTime.Today; + + foreach (var kvp in state.Value.Targets) + { + var target = kvp.Value; + + var from = GetFromDate(today, target.NumDays); + + if (!target.Triggered.HasValue || target.Triggered < from) + { + var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); + + var limit = kvp.Value.Limits; + + if (usage > limit) + { + kvp.Value.Triggered = today; + + var @event = new AppUsageExceeded + { + AppId = target.AppId, + CallsCurrent = usage, + CallsLimit = limit, + RuleId = kvp.Key + }; + + await state.WriteEventAsync(Envelope.Create(@event)); + } + } + } + + await state.WriteAsync(); + } + + private static DateTime GetFromDate(DateTime today, int? numDays) + { + if (numDays.HasValue) + { + return today.AddDays(-numDays.Value).AddDays(1); + } + else + { + return new DateTime(today.Year, today.Month, 1); + } + } + + public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); + + return state.WriteAsync(); + } + + public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); + + return state.WriteAsync(); + } + + public Task AddTargetAsync(Guid ruleId, int limits) + { + UpdateTarget(ruleId, t => t.Limits = limits); + + return state.WriteAsync(); + } + + public Task RemoveTargetAsync(Guid ruleId) + { + state.Value.Targets.Remove(ruleId); + + return state.WriteAsync(); + } + + private void UpdateTarget(Guid ruleId, Action updater) + { + updater(state.Value.Targets.GetOrAddNew(ruleId)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs new file mode 100644 index 000000000..ae2154284 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTriggerHandler : RuleTriggerHandler + { + private const string EventName = "Usage exceeded"; + + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedUsageExceededEvent + { + CallsCurrent = @event.Payload.CallsCurrent, + CallsLimit = @event.Payload.CallsLimit, + Name = EventName + }; + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) + { + return @event.CallsLimit == trigger.Limit; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs new file mode 100644 index 000000000..a62306c64 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class BackupSchemas : BackupHandler + { + private readonly Dictionary schemasByName = new Dictionary(); + private readonly ISchemasIndex indexSchemas; + + public override string Name { get; } = "Schemas"; + + public BackupSchemas(ISchemasIndex indexSchemas) + { + Guard.NotNull(indexSchemas); + + this.indexSchemas = indexSchemas; + } + + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) + { + switch (@event.Payload) + { + case SchemaCreated schemaCreated: + schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id; + break; + case SchemaDeleted schemaDeleted: + schemasByName.Remove(schemaDeleted.SchemaId.Name); + break; + } + + return TaskHelper.True; + } + + public override Task RestoreAsync(Guid appId, BackupReader reader) + { + return indexSchemas.RebuildAsync(appId, schemasByName); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs new file mode 100644 index 000000000..a009957f8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -0,0 +1,251 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public static class GuardSchema + { + public static void CanCreate(CreateSchema command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot create schema.", e => + { + if (!command.Name.IsSlug()) + { + e(Not.ValidSlug("Name"), nameof(command.Name)); + } + + ValidateUpsert(command, e); + }); + } + + public static void CanSynchronize(SynchronizeSchema command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot synchronize schema.", e => + { + ValidateUpsert(command, e); + }); + } + + public static void CanReorder(Schema schema, ReorderFields command) + { + Guard.NotNull(command); + + IArrayField? arrayField = null; + + if (command.ParentFieldId.HasValue) + { + arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + } + + Validate.It(() => "Cannot reorder schema fields.", error => + { + if (command.FieldIds == null) + { + error("Field ids is required.", nameof(command.FieldIds)); + } + + if (arrayField == null) + { + ValidateFieldIds(error, command, schema.FieldsById); + } + else + { + ValidateFieldIds(error, command, arrayField.FieldsById); + } + }); + } + + public static void CanConfigurePreviewUrls(ConfigurePreviewUrls command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot configure preview urls.", error => + { + if (command.PreviewUrls == null) + { + error("Preview Urls is required.", nameof(command.PreviewUrls)); + } + }); + } + + public static void CanPublish(Schema schema, PublishSchema command) + { + Guard.NotNull(command); + + if (schema.IsPublished) + { + throw new DomainException("Schema is already published."); + } + } + + public static void CanUnpublish(Schema schema, UnpublishSchema command) + { + Guard.NotNull(command); + + if (!schema.IsPublished) + { + throw new DomainException("Schema is not published."); + } + } + + public static void CanUpdate(Schema schema, UpdateSchema command) + { + Guard.NotNull(command); + } + + public static void CanConfigureScripts(Schema schema, ConfigureScripts command) + { + Guard.NotNull(command); + } + + public static void CanChangeCategory(Schema schema, ChangeCategory command) + { + Guard.NotNull(command); + } + + public static void CanDelete(Schema schema, DeleteSchema command) + { + Guard.NotNull(command); + } + + private static void ValidateUpsert(UpsertCommand command, AddValidation e) + { + if (command.Fields?.Count > 0) + { + var fieldIndex = 0; + var fieldPrefix = string.Empty; + + foreach (var field in command.Fields) + { + fieldIndex++; + fieldPrefix = $"Fields[{fieldIndex}]"; + + ValidateRootField(field, fieldPrefix, e); + } + + if (command.Fields.Select(x => x?.Name).Distinct().Count() != command.Fields.Count) + { + e("Fields cannot have duplicate names.", nameof(command.Fields)); + } + } + } + + private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) + { + if (field == null) + { + e(Not.Defined("Field"), prefix); + } + else + { + if (!field.Partitioning.IsValidPartitioning()) + { + e(Not.Valid("Partitioning"), $"{prefix}.{nameof(field.Partitioning)}"); + } + + ValidateField(field, prefix, e); + + if (field.Nested?.Count > 0) + { + if (field.Properties is ArrayFieldProperties) + { + var nestedIndex = 0; + var nestedPrefix = string.Empty; + + foreach (var nestedField in field.Nested) + { + nestedIndex++; + nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; + + ValidateNestedField(nestedField, nestedPrefix, e); + } + } + else if (field.Nested.Count > 0) + { + e("Only array fields can have nested fields.", $"{prefix}.{nameof(field.Partitioning)}"); + } + + if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count) + { + e("Fields cannot have duplicate names.", $"{prefix}.Nested"); + } + } + } + } + + private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) + { + if (nestedField == null) + { + e(Not.Defined("Field"), prefix); + } + else + { + if (nestedField.Properties is ArrayFieldProperties) + { + e("Nested field cannot be array fields.", $"{prefix}.{nameof(nestedField.Properties)}"); + } + + ValidateField(nestedField, prefix, e); + } + } + + private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e) + { + if (!field.Name.IsPropertyName()) + { + e("Field name must be a valid javascript property name.", $"{prefix}.{nameof(field.Name)}"); + } + + if (field.Properties == null) + { + e(Not.Defined("Field properties"), $"{prefix}.{nameof(field.Properties)}"); + } + else + { + if (!field.Properties.IsForApi()) + { + if (field.IsHidden) + { + e("UI field cannot be hidden.", $"{prefix}.{nameof(field.IsHidden)}"); + } + + if (field.IsDisabled) + { + e("UI field cannot be disabled.", $"{prefix}.{nameof(field.IsDisabled)}"); + } + } + + var errors = FieldPropertiesValidator.Validate(field.Properties); + + errors.Foreach(x => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); + } + } + + private static void ValidateFieldIds(AddValidation error, ReorderFields c, IReadOnlyDictionary fields) + { + if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x)))) + { + error("Field ids do not cover all fields.", nameof(c.FieldIds)); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs new file mode 100644 index 000000000..f99ffd53d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public static class GuardSchemaField + { + public static void CanAdd(Schema schema, AddField command) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot add a new field.", e => + { + if (!command.Name.IsPropertyName()) + { + e("Name must be a valid javascript property name.", nameof(command.Name)); + } + + if (command.Properties == null) + { + e(Not.Defined("Properties"), nameof(command.Properties)); + } + else + { + var errors = FieldPropertiesValidator.Validate(command.Properties); + + errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + } + + if (command.ParentFieldId.HasValue) + { + var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + + if (arrayField.FieldsByName.ContainsKey(command.Name)) + { + e("A field with the same name already exists."); + } + } + else + { + if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning()) + { + e(Not.Valid("Partitioning"), nameof(command.Partitioning)); + } + + if (schema.FieldsByName.ContainsKey(command.Name)) + { + e("A field with the same name already exists."); + } + } + }); + } + + public static void CanUpdate(Schema schema, UpdateField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + Validate.It(() => "Cannot update field.", e => + { + if (command.Properties == null) + { + e(Not.Defined("Properties"), nameof(command.Properties)); + } + else + { + var errors = FieldPropertiesValidator.Validate(command.Properties); + + errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + } + }); + } + + public static void CanHide(Schema schema, HideField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsHidden) + { + throw new DomainException("Schema field is already hidden."); + } + + if (!field.IsForApi()) + { + throw new DomainException("UI field cannot be hidden."); + } + } + + public static void CanShow(Schema schema, ShowField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (!field.IsHidden) + { + throw new DomainException("Schema field is already visible."); + } + } + + public static void CanDisable(Schema schema, DisableField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsDisabled) + { + throw new DomainException("Schema field is already disabled."); + } + + if (!field.IsForApi(true)) + { + throw new DomainException("UI field cannot be disabled."); + } + } + + public static void CanDelete(Schema schema, DeleteField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsLocked) + { + throw new DomainException("Schema field is locked."); + } + } + + public static void CanEnable(Schema schema, EnableField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (!field.IsDisabled) + { + throw new DomainException("Schema field is already enabled."); + } + } + + public static void CanLock(Schema schema, LockField command) + { + Guard.NotNull(command); + + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + + if (field.IsLocked) + { + throw new DomainException("Schema field is already locked."); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs new file mode 100644 index 000000000..0031ffe31 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public interface ISchemasIndex + { + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); + + Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false); + + Task> GetSchemasAsync(Guid appId, bool allowDeleted = false); + + Task RebuildAsync(Guid appId, Dictionary schemas); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs new file mode 100644 index 000000000..a0159e0d9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -0,0 +1,181 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex + { + private readonly IGrainFactory grainFactory; + + public SchemasIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public Task RebuildAsync(Guid appId, Dictionary schemas) + { + return Index(appId).RebuildAsync(schemas); + } + + public async Task> GetSchemasAsync(Guid appId, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var ids = await GetSchemaIdsAsync(appId); + + var schemas = + await Task.WhenAll( + ids.Select(id => GetSchemaAsync(appId, id, allowDeleted))); + + return schemas.Where(x => x != null).ToList(); + } + } + + public async Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var id = await GetSchemaIdAsync(appId, name); + + if (id == default) + { + return null; + } + + return await GetSchemaAsync(appId, id, allowDeleted); + } + } + + public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var schema = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(schema.Value, allowDeleted)) + { + return schema.Value; + } + + return null; + } + } + + private async Task GetSchemaIdAsync(Guid appId, string name) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdAsync(name); + } + } + + private async Task> GetSchemaIdsAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdsAsync(); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is CreateSchema createSchema) + { + var index = Index(createSchema.AppId.Id); + + var token = await CheckSchemaAsync(index, createSchema); + + try + { + await next(); + } + finally + { + if (token != null) + { + if (context.IsCompleted) + { + await index.AddAsync(token); + } + else + { + await index.RemoveReservationAsync(token); + } + } + } + } + else + { + await next(); + + if (context.IsCompleted) + { + if (context.Command is DeleteSchema deleteSchema) + { + await DeleteSchemaAsync(deleteSchema); + } + } + } + } + + private async Task CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command) + { + var name = command.Name; + + if (name.IsSlug()) + { + var token = await index.ReserveAsync(command.SchemaId, name); + + if (token == null) + { + var error = new ValidationError("A schema with this name already exists."); + + throw new ValidationException("Cannot create schema.", error); + } + + return token; + } + + return null; + } + + private async Task DeleteSchemaAsync(DeleteSchema commmand) + { + var schemaId = commmand.SchemaId; + + var schema = await grainFactory.GetGrain(schemaId).GetStateAsync(); + + if (IsFound(schema.Value, true)) + { + await Index(schema.Value.AppId.Id).RemoveAsync(schemaId); + } + } + + private ISchemasByAppIndexGrain Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + + private static bool IsFound(ISchemaEntity entity, bool allowDeleted) + { + return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs new file mode 100644 index 000000000..f78db7adb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + + public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) + { + Guard.NotNull(scriptEngine); + + this.scriptEngine = scriptEngine; + } + + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + EnrichedSchemaEvent? result = new EnrichedSchemaEvent(); + + SimpleMapper.Map(@event.Payload, result); + + switch (@event.Payload) + { + case FieldEvent _: + case SchemaPreviewUrlsConfigured _: + case SchemaScriptsConfigured _: + case SchemaUpdated _: + case ParentFieldEvent _: + result.Type = EnrichedSchemaEventType.Updated; + break; + case SchemaCreated _: + result.Type = EnrichedSchemaEventType.Created; + break; + case SchemaPublished _: + result.Type = EnrichedSchemaEventType.Published; + break; + case SchemaUnpublished _: + result.Type = EnrichedSchemaEventType.Unpublished; + break; + case SchemaDeleted _: + result.Type = EnrichedSchemaEventType.Deleted; + break; + default: + result = null; + break; + } + + if (result != null) + { + result.Name = $"Schema{result.Type}"; + } + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) + { + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs new file mode 100644 index 000000000..f4fecddfc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -0,0 +1,417 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.EventSynchronization; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Guards; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain + { + private readonly IJsonSerializer serializer; + + public SchemaGrain(IStore store, ISemanticLog log, IJsonSerializer serializer) + : base(store, log) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case AddField addField: + return UpdateReturn(addField, c => + { + GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); + + Add(c); + + long id; + + if (c.ParentFieldId == null) + { + id = Snapshot.SchemaDef.FieldsByName[c.Name].Id; + } + else + { + id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id; + } + + return Snapshot; + }); + + case CreateSchema createSchema: + return CreateReturn(createSchema, c => + { + GuardSchema.CanCreate(c); + + Create(c); + + return Snapshot; + }); + + case SynchronizeSchema synchronizeSchema: + return UpdateReturn(synchronizeSchema, c => + { + GuardSchema.CanSynchronize(c); + + Synchronize(c); + + return Snapshot; + }); + + case DeleteField deleteField: + return UpdateReturn(deleteField, c => + { + GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); + + DeleteField(c); + + return Snapshot; + }); + + case LockField lockField: + return UpdateReturn(lockField, c => + { + GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); + + LockField(c); + + return Snapshot; + }); + + case HideField hideField: + return UpdateReturn(hideField, c => + { + GuardSchemaField.CanHide(Snapshot.SchemaDef, c); + + HideField(c); + + return Snapshot; + }); + + case ShowField showField: + return UpdateReturn(showField, c => + { + GuardSchemaField.CanShow(Snapshot.SchemaDef, c); + + ShowField(c); + + return Snapshot; + }); + + case DisableField disableField: + return UpdateReturn(disableField, c => + { + GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); + + DisableField(c); + + return Snapshot; + }); + + case EnableField enableField: + return UpdateReturn(enableField, c => + { + GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); + + EnableField(c); + + return Snapshot; + }); + + case UpdateField updateField: + return UpdateReturn(updateField, c => + { + GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); + + UpdateField(c); + + return Snapshot; + }); + + case ReorderFields reorderFields: + return UpdateReturn(reorderFields, c => + { + GuardSchema.CanReorder(Snapshot.SchemaDef, c); + + Reorder(c); + + return Snapshot; + }); + + case UpdateSchema updateSchema: + return UpdateReturn(updateSchema, c => + { + GuardSchema.CanUpdate(Snapshot.SchemaDef, c); + + Update(c); + + return Snapshot; + }); + + case PublishSchema publishSchema: + return UpdateReturn(publishSchema, c => + { + GuardSchema.CanPublish(Snapshot.SchemaDef, c); + + Publish(c); + + return Snapshot; + }); + + case UnpublishSchema unpublishSchema: + return UpdateReturn(unpublishSchema, c => + { + GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); + + Unpublish(c); + + return Snapshot; + }); + + case ConfigureScripts configureScripts: + return UpdateReturn(configureScripts, c => + { + GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); + + ConfigureScripts(c); + + return Snapshot; + }); + + case ChangeCategory changeCategory: + return UpdateReturn(changeCategory, c => + { + GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c); + + ChangeCategory(c); + + return Snapshot; + }); + + case ConfigurePreviewUrls configurePreviewUrls: + return UpdateReturn(configurePreviewUrls, c => + { + GuardSchema.CanConfigurePreviewUrls(c); + + ConfigurePreviewUrls(c); + + return Snapshot; + }); + + case DeleteSchema deleteSchema: + return Update(deleteSchema, c => + { + GuardSchema.CanDelete(Snapshot.SchemaDef, c); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + public void Synchronize(SynchronizeSchema command) + { + var options = new SchemaSynchronizationOptions + { + NoFieldDeletion = command.NoFieldDeletion, + NoFieldRecreation = command.NoFieldRecreation + }; + + var schemaSource = Snapshot.SchemaDef; + var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); + + var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); + + foreach (var @event in events) + { + RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); + } + } + + public void Create(CreateSchema command) + { + RaiseEvent(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.ToSchema() }); + } + + public void Add(AddField command) + { + RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) }); + } + + public void UpdateField(UpdateField command) + { + RaiseEvent(command, new FieldUpdated()); + } + + public void LockField(LockField command) + { + RaiseEvent(command, new FieldLocked()); + } + + public void HideField(HideField command) + { + RaiseEvent(command, new FieldHidden()); + } + + public void ShowField(ShowField command) + { + RaiseEvent(command, new FieldShown()); + } + + public void DisableField(DisableField command) + { + RaiseEvent(command, new FieldDisabled()); + } + + public void EnableField(EnableField command) + { + RaiseEvent(command, new FieldEnabled()); + } + + public void DeleteField(DeleteField command) + { + RaiseEvent(command, new FieldDeleted()); + } + + public void Reorder(ReorderFields command) + { + RaiseEvent(command, new SchemaFieldsReordered()); + } + + public void Publish(PublishSchema command) + { + RaiseEvent(command, new SchemaPublished()); + } + + public void Unpublish(UnpublishSchema command) + { + RaiseEvent(command, new SchemaUnpublished()); + } + + public void ConfigureScripts(ConfigureScripts command) + { + RaiseEvent(command, new SchemaScriptsConfigured()); + } + + public void ChangeCategory(ChangeCategory command) + { + RaiseEvent(command, new SchemaCategoryChanged()); + } + + public void ConfigurePreviewUrls(ConfigurePreviewUrls command) + { + RaiseEvent(command, new SchemaPreviewUrlsConfigured()); + } + + public void Update(UpdateSchema command) + { + RaiseEvent(command, new SchemaUpdated()); + } + + public void Delete(DeleteSchema command) + { + RaiseEvent(command, new SchemaDeleted()); + } + + private void RaiseEvent(TCommand command, TEvent @event) where TCommand : SchemaCommand where TEvent : SchemaEvent + { + SimpleMapper.Map(command, @event); + + NamedId? GetFieldId(long? id) + { + if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) + { + return field.NamedId(); + } + + return null; + } + + if (command is ParentFieldCommand pc && @event is ParentFieldEvent pe) + { + if (pc.ParentFieldId.HasValue) + { + if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field)) + { + pe.ParentFieldId = field.NamedId(); + + if (command is FieldCommand fc && @event is FieldEvent fe) + { + if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) + { + fe.FieldId = nestedField.NamedId(); + } + } + } + } + else if (command is FieldCommand fc && @event is FieldEvent fe) + { + fe.FieldId = GetFieldId(fc.FieldId)!; + } + } + + RaiseEvent(@event); + } + + private void RaiseEvent(SchemaEvent @event) + { + if (@event.SchemaId == null) + { + @event.SchemaId = Snapshot.NamedId(); + } + + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private NamedId CreateFieldId(AddField command) + { + return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Schema has already been deleted."); + } + } + + public Task> GetStateAsync() + { + return J.AsTask(Snapshot); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs new file mode 100644 index 000000000..2807562d1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase + { + public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "reordered fields of schema {[Name]}."); + + AddEventMessage( + "created schema {[Name]}."); + + AddEventMessage( + "updated schema {[Name]}."); + + AddEventMessage( + "deleted schema {[Name]}."); + + AddEventMessage( + "published schema {[Name]}."); + + AddEventMessage( + "unpublished schema {[Name]}."); + + AddEventMessage( + "reordered fields of schema {[Name]}."); + + AddEventMessage( + "configured script of schema {[Name]}."); + + AddEventMessage( + "added field {[Field]} to schema {[Name]}."); + + AddEventMessage( + "deleted field {[Field]} from schema {[Name]}."); + + AddEventMessage( + "has locked field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "has hidden field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "has shown field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "disabled field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "disabled field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "has updated field {[Field]} of schema {[Name]}."); + + AddEventMessage( + "deleted field {[Field]} of schema {[Name]}."); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + HistoryEvent? result = null; + + if (@event.Payload is SchemaEvent schemaEvent) + { + var channel = $"schemas.{schemaEvent.SchemaId.Name}"; + + result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); + + if (schemaEvent is FieldEvent fieldEvent) + { + result.Param("Field", fieldEvent.FieldId.Name); + } + } + + return Task.FromResult(result); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj new file mode 100644 index 000000000..0deed8bfd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -0,0 +1,41 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/SquidexCommand.cs rename to backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/SquidexEntities.cs rename to backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs diff --git a/src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs new file mode 100644 index 000000000..6efb9e4d6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class GrainTagService : ITagService + { + private readonly IGrainFactory grainFactory; + + public string Name + { + get { return "Tags"; } + } + + public GrainTagService(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public Task> NormalizeTagsAsync(Guid appId, string group, HashSet? names, HashSet? ids) + { + return GetGrain(appId, group).NormalizeTagsAsync(names, ids); + } + + public Task> GetTagIdsAsync(Guid appId, string group, HashSet names) + { + return GetGrain(appId, group).GetTagIdsAsync(names); + } + + public Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids) + { + return GetGrain(appId, group).DenormalizeTagsAsync(ids); + } + + public Task GetTagsAsync(Guid appId, string group) + { + return GetGrain(appId, group).GetTagsAsync(); + } + + public Task GetExportableTagsAsync(Guid appId, string group) + { + return GetGrain(appId, group).GetExportableTagsAsync(); + } + + public Task RebuildTagsAsync(Guid appId, string group, TagsExport tags) + { + return GetGrain(appId, group).RebuildAsync(tags); + } + + public Task ClearAsync(Guid appId, string group) + { + return GetGrain(appId, group).ClearAsync(); + } + + private ITagGrain GetGrain(Guid appId, string group) + { + Guard.NotNullOrEmpty(group); + + return grainFactory.GetGrain($"{appId}_{group}"); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs similarity index 100% rename from src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs new file mode 100644 index 000000000..a1cd13b9d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Tags; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public interface ITagGrain : IGrainWithStringKey + { + Task> NormalizeTagsAsync(HashSet? names, HashSet? ids); + + Task> GetTagIdsAsync(HashSet names); + + Task> DenormalizeTagsAsync(HashSet ids); + + Task GetTagsAsync(); + + Task GetExportableTagsAsync(); + + Task ClearAsync(); + + Task RebuildAsync(TagsExport tags); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs new file mode 100644 index 000000000..32240fbdc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -0,0 +1,152 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class TagGrain : GrainOfString, ITagGrain + { + private readonly IGrainState state; + + [CollectionName("Index_Tags")] + public sealed class GrainState + { + public TagsExport Tags { get; set; } = new TagsExport(); + } + + public TagGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task ClearAsync() + { + return state.ClearAsync(); + } + + public Task RebuildAsync(TagsExport tags) + { + state.Value.Tags = tags; + + return state.WriteAsync(); + } + + public async Task> NormalizeTagsAsync(HashSet? names, HashSet? ids) + { + var result = new Dictionary(); + + if (names != null) + { + foreach (var tag in names) + { + if (!string.IsNullOrWhiteSpace(tag)) + { + var tagName = tag.ToLowerInvariant(); + var tagId = string.Empty; + + var found = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); + + if (found.Value != null) + { + tagId = found.Key; + + if (ids == null || !ids.Contains(tagId)) + { + found.Value.Count++; + } + } + else + { + tagId = Guid.NewGuid().ToString(); + + state.Value.Tags.Add(tagId, new Tag { Name = tagName }); + } + + result.Add(tagName, tagId); + } + } + } + + if (ids != null) + { + foreach (var id in ids) + { + if (!result.ContainsValue(id)) + { + if (state.Value.Tags.TryGetValue(id, out var tagInfo)) + { + tagInfo.Count--; + + if (tagInfo.Count <= 0) + { + state.Value.Tags.Remove(id); + } + } + } + } + } + + await state.WriteAsync(); + + return result; + } + + public Task> GetTagIdsAsync(HashSet names) + { + var result = new Dictionary(); + + foreach (var name in names) + { + var id = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; + + if (!string.IsNullOrWhiteSpace(id)) + { + result.Add(name, id); + } + } + + return Task.FromResult(result); + } + + public Task> DenormalizeTagsAsync(HashSet ids) + { + var result = new Dictionary(); + + foreach (var id in ids) + { + if (state.Value.Tags.TryGetValue(id, out var tagInfo)) + { + result[id] = tagInfo.Name; + } + } + + return Task.FromResult(result); + } + + public Task GetTagsAsync() + { + var tags = state.Value.Tags.Values.ToDictionary(x => x.Name, x => x.Count); + + return Task.FromResult(new TagsSet(tags, state.Version)); + } + + public Task GetExportableTagsAsync() + { + return Task.FromResult(state.Value.Tags); + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/AppEvent.cs b/backend/src/Squidex.Domain.Apps.Events/AppEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/AppEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/AppEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs b/backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs rename to backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs new file mode 100644 index 000000000..8635344e5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternAdded))] + public sealed class AppPatternAdded : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs new file mode 100644 index 000000000..cc898cc6c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternUpdated))] + public sealed class AppPatternUpdated : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string? Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs new file mode 100644 index 000000000..b5ecfcb00 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetAnnotated))] + public sealed class AssetAnnotated : AssetEvent + { + public string FileName { get; set; } + + public string Slug { get; set; } + + public HashSet? Tags { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs new file mode 100644 index 000000000..ef7173832 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetCreated))] + public sealed class AssetCreated : AssetEvent + { + public string FileName { get; set; } + + public string FileHash { get; set; } + + public string MimeType { get; set; } + + public string Slug { get; set; } + + public long FileVersion { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + + public HashSet? Tags { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesDiscarded.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentChangesPublished.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdateProposed.cs diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs diff --git a/src/Squidex.Domain.Apps.Events/SchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SchemaEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs new file mode 100644 index 000000000..3bec00f1a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Schemas +{ + [EventType(nameof(FieldAdded))] + public sealed class FieldAdded : FieldEvent + { + public string Name { get; set; } + + public string? Partitioning { get; set; } + + public FieldProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs new file mode 100644 index 000000000..01bcb1e5c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Events.Schemas +{ + public abstract class ParentFieldEvent : SchemaEvent + { + public NamedId? ParentFieldId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs diff --git a/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs rename to backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj new file mode 100644 index 000000000..d363a9005 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexEvent.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexEvents.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs similarity index 100% rename from src/Squidex.Domain.Apps.Events/SquidexHeaders.cs rename to backend/src/Squidex.Domain.Apps.Events/SquidexHeaders.cs diff --git a/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs similarity index 100% rename from src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs rename to backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs diff --git a/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs similarity index 100% rename from src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs rename to backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs new file mode 100644 index 000000000..6a8bfbdd9 --- /dev/null +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUser : IdentityUser + { + public List Claims { get; set; } = new List(); + + public List Tokens { get; set; } = new List(); + + public List Logins { get; set; } = new List(); + + public HashSet Roles { get; set; } = new HashSet(); + + internal void AddLogin(UserLoginInfo login) + { + Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName)); + } + + internal void AddRole(string role) + { + Roles.Add(role); + } + + internal void RemoveRole(string role) + { + Roles.Remove(role); + } + + internal void RemoveLogin(string loginProvider, string providerKey) + { + Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + } + + internal void AddClaim(Claim claim) + { + Claims.Add(claim); + } + + internal void AddClaims(IEnumerable claims) + { + claims.Foreach(AddClaim); + } + + internal void RemoveClaim(Claim claim) + { + Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); + } + + internal void RemoveClaims(IEnumerable claims) + { + claims.Foreach(RemoveClaim); + } + + internal string? GetToken(string loginProvider, string name) + { + return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value; + } + + internal void AddToken(string loginProvider, string name, string value) + { + Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value }); + } + + internal void RemoveToken(string loginProvider, string name) + { + Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); + } + + internal void ReplaceClaim(Claim existingClaim, Claim newClaim) + { + RemoveClaim(existingClaim); + + AddClaim(newClaim); + } + + internal void SetToken(string loginProider, string name, string value) + { + RemoveToken(loginProider, name); + + AddToken(loginProider, name, value); + } + } + + public sealed class UserTokenInfo : IdentityUserToken + { + } +} diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs new file mode 100644 index 000000000..b0900434e --- /dev/null +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -0,0 +1,526 @@ +// ========================================================================== +// 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 System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUserStore : + MongoRepositoryBase, + IUserAuthenticationTokenStore, + IUserAuthenticatorKeyStore, + IUserClaimStore, + IUserEmailStore, + IUserFactory, + IUserLockoutStore, + IUserLoginStore, + IUserPasswordStore, + IUserPhoneNumberStore, + IUserRoleStore, + IUserSecurityStampStore, + IUserTwoFactorStore, + IUserTwoFactorRecoveryCodeStore, + IQueryableUserStore + { + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; + + static MongoUserStore() + { + BsonClassMap.RegisterClassMap(cm => + { + cm.MapConstructor(typeof(Claim).GetConstructors() + .First(x => + { + var parameters = x.GetParameters(); + + return parameters.Length == 2 && + parameters[0].Name == "type" && + parameters[0].ParameterType == typeof(string) && + parameters[1].Name == "value" && + parameters[1].ParameterType == typeof(string); + })) + .SetArguments(new[] + { + nameof(Claim.Type), + nameof(Claim.Value) + }); + + cm.MapMember(x => x.Type); + cm.MapMember(x => x.Value); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.MapConstructor(typeof(UserLoginInfo).GetConstructors().First()) + .SetArguments(new[] + { + nameof(UserLoginInfo.LoginProvider), + nameof(UserLoginInfo.ProviderKey), + nameof(UserLoginInfo.ProviderDisplayName) + }); + + cm.AutoMap(); + }); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.UnmapMember(x => x.UserId); + }); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId)); + cm.MapMember(x => x.AccessFailedCount).SetIgnoreIfDefault(true); + cm.MapMember(x => x.EmailConfirmed).SetIgnoreIfDefault(true); + cm.MapMember(x => x.LockoutEnd).SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); + cm.MapMember(x => x.LockoutEnabled).SetIgnoreIfDefault(true); + cm.MapMember(x => x.PasswordHash).SetIgnoreIfNull(true); + cm.MapMember(x => x.PhoneNumber).SetIgnoreIfNull(true); + cm.MapMember(x => x.PhoneNumberConfirmed).SetIgnoreIfDefault(true); + cm.MapMember(x => x.SecurityStamp).SetIgnoreIfNull(true); + cm.MapMember(x => x.TwoFactorEnabled).SetIgnoreIfDefault(true); + }); + } + + public MongoUserStore(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Identity_Users"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending("Logins.LoginProvider") + .Ascending("Logins.ProviderKey")), + new CreateIndexModel( + Index + .Ascending(x => x.NormalizedUserName), + new CreateIndexOptions + { + Unique = true + }), + new CreateIndexModel( + Index + .Ascending(x => x.NormalizedEmail), + new CreateIndexOptions + { + Unique = true + }) + }, ct); + } + + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; + } + + public void Dispose() + { + } + + public IQueryable Users + { + get { return Collection.AsQueryable(); } + } + + public bool IsId(string id) + { + return ObjectId.TryParse(id, out _); + } + + public IdentityUser Create(string email) + { + return new MongoUser { Email = email, UserName = email }; + } + + public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + if (!IsId(userId)) + { + return null!; + } + + return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); + } + + public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); + } + + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); + } + + public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); + } + + public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + { + return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); + } + + public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + { + return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); + } + + public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) + { + user.Id = ObjectId.GenerateNewId().ToString(); + + await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); + + return IdentityResult.Success; + } + + public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) + { + await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); + + return IdentityResult.Success; + } + + public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) + { + await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); + + return IdentityResult.Success; + } + + public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).Id); + } + + public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).UserName); + } + + public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).NormalizedUserName); + } + + public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).PasswordHash); + } + + public Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult>(((MongoUser)user).Roles.ToList()); + } + + public Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); + } + + public Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList()); + } + + public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).SecurityStamp); + } + + public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).Email); + } + + public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).EmailConfirmed); + } + + public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).NormalizedEmail); + } + + public Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult>(((MongoUser)user).Claims); + } + + public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).PhoneNumber); + } + + public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); + } + + public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).TwoFactorEnabled); + } + + public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).LockoutEnd); + } + + public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).AccessFailedCount); + } + + public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).LockoutEnabled); + } + + public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)!); + } + + public Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!); + } + + public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); + } + + public Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0); + } + + public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken) + { + ((MongoUser)user).UserName = userName; + + return TaskHelper.Done; + } + + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken) + { + ((MongoUser)user).NormalizedUserName = normalizedName; + + return TaskHelper.Done; + } + + public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken) + { + ((MongoUser)user).PasswordHash = passwordHash; + + return TaskHelper.Done; + } + + public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + { + ((MongoUser)user).AddRole(roleName); + + return TaskHelper.Done; + } + + public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveRole(roleName); + + return TaskHelper.Done; + } + + public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) + { + ((MongoUser)user).AddLogin(login); + + return TaskHelper.Done; + } + + public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveLogin(loginProvider, providerKey); + + return TaskHelper.Done; + } + + public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) + { + ((MongoUser)user).SecurityStamp = stamp; + + return TaskHelper.Done; + } + + public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken) + { + ((MongoUser)user).Email = email; + + return TaskHelper.Done; + } + + public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) + { + ((MongoUser)user).EmailConfirmed = confirmed; + + return TaskHelper.Done; + } + + public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + { + ((MongoUser)user).NormalizedEmail = normalizedEmail; + + return TaskHelper.Done; + } + + public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) + { + ((MongoUser)user).AddClaims(claims); + + return TaskHelper.Done; + } + + public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + { + ((MongoUser)user).ReplaceClaim(claim, newClaim); + + return TaskHelper.Done; + } + + public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveClaims(claims); + + return TaskHelper.Done; + } + + public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken) + { + ((MongoUser)user).PhoneNumber = phoneNumber; + + return TaskHelper.Done; + } + + public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) + { + ((MongoUser)user).PhoneNumberConfirmed = confirmed; + + return TaskHelper.Done; + } + + public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) + { + ((MongoUser)user).TwoFactorEnabled = enabled; + + return TaskHelper.Done; + } + + public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + { + ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; + + return TaskHelper.Done; + } + + public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + { + ((MongoUser)user).AccessFailedCount++; + + return Task.FromResult(((MongoUser)user).AccessFailedCount); + } + + public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + { + ((MongoUser)user).AccessFailedCount = 0; + + return TaskHelper.Done; + } + + public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) + { + ((MongoUser)user).LockoutEnabled = enabled; + + return TaskHelper.Done; + } + + public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(loginProvider, name, value); + + return TaskHelper.Done; + } + + public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveToken(loginProvider, name); + + return TaskHelper.Done; + } + + public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); + + return TaskHelper.Done; + } + + public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); + + return TaskHelper.Done; + } + + public Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) + { + var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; + + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) + { + var updatedCodes = new List(splitCodes.Where(s => s != code)); + + ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); + + return TaskHelper.True; + } + + return TaskHelper.False; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj new file mode 100644 index 000000000..3cab46969 --- /dev/null +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -0,0 +1,32 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs b/backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs new file mode 100644 index 000000000..48b364886 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/AssetUserPictureStore.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Users +{ + public sealed class AssetUserPictureStore : IUserPictureStore + { + private readonly IAssetStore assetStore; + + public AssetUserPictureStore(IAssetStore assetStore) + { + Guard.NotNull(assetStore); + + this.assetStore = assetStore; + } + + public Task UploadAsync(string userId, Stream stream) + { + return assetStore.UploadAsync(userId, 0, "picture", stream, true); + } + + public async Task DownloadAsync(string userId) + { + var memoryStream = new MemoryStream(); + + await assetStore.DownloadAsync(userId, 0, "picture", memoryStream); + + memoryStream.Position = 0; + + return memoryStream; + } + } +} diff --git a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs new file mode 100644 index 000000000..491d0c59e --- /dev/null +++ b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class DefaultUserResolver : IUserResolver + { + private readonly IServiceProvider serviceProvider; + + public DefaultUserResolver(IServiceProvider serviceProvider) + { + Guard.NotNull(serviceProvider); + + this.serviceProvider = serviceProvider; + } + + public async Task CreateUserIfNotExists(string email, bool invited) + { + using (var scope = serviceProvider.CreateScope()) + { + var userFactory = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var user = userFactory.Create(email); + + try + { + var result = await userManager.CreateAsync(user); + + if (result.Succeeded) + { + var values = new UserValues { DisplayName = email, Invited = invited }; + + await userManager.UpdateAsync(user, values); + } + + return result.Succeeded; + } + catch + { + return false; + } + } + } + + public async Task FindByIdOrEmailAsync(string idOrEmail) + { + using (var scope = serviceProvider.CreateScope()) + { + var userFactory = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + if (userFactory.IsId(idOrEmail)) + { + return await userManager.FindByIdWithClaimsAsync(idOrEmail); + } + else + { + return await userManager.FindByEmailWithClaimsAsyncAsync(idOrEmail); + } + } + } + + public async Task> QueryByEmailAsync(string email) + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var result = await userManager.QueryByEmailAsync(email); + + return result.OfType().ToList(); + } + } + + public async Task> QueryManyAsync(string[] ids) + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var result = await userManager.QueryByIdsAync(ids); + + return result.OfType().ToDictionary(x => x.Id); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs new file mode 100644 index 000000000..621d45153 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Xml.Linq; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Users +{ + public sealed class DefaultXmlRepository : IXmlRepository + { + private readonly ISnapshotStore store; + + [CollectionName("XmlRepository")] + public sealed class State + { + public string Xml { get; set; } + } + + public DefaultXmlRepository(ISnapshotStore store) + { + Guard.NotNull(store); + + this.store = store; + } + + public IReadOnlyCollection GetAllElements() + { + var result = new List(); + + store.ReadAllAsync((state, version) => + { + result.Add(XElement.Parse(state.Xml)); + + return TaskHelper.Done; + }).Wait(); + + return result; + } + + public void StoreElement(XElement element, string friendlyName) + { + store.WriteAsync(friendlyName, new State { Xml = element.ToString() }, EtagVersion.Any, EtagVersion.Any).Wait(); + } + } +} diff --git a/src/Squidex.Domain.Users/IUserEvents.cs b/backend/src/Squidex.Domain.Users/IUserEvents.cs similarity index 100% rename from src/Squidex.Domain.Users/IUserEvents.cs rename to backend/src/Squidex.Domain.Users/IUserEvents.cs diff --git a/src/Squidex.Domain.Users/IUserFactory.cs b/backend/src/Squidex.Domain.Users/IUserFactory.cs similarity index 100% rename from src/Squidex.Domain.Users/IUserFactory.cs rename to backend/src/Squidex.Domain.Users/IUserFactory.cs diff --git a/src/Squidex.Domain.Users/IUserPictureStore.cs b/backend/src/Squidex.Domain.Users/IUserPictureStore.cs similarity index 100% rename from src/Squidex.Domain.Users/IUserPictureStore.cs rename to backend/src/Squidex.Domain.Users/IUserPictureStore.cs diff --git a/src/Squidex.Domain.Users/NoopUserEvents.cs b/backend/src/Squidex.Domain.Users/NoopUserEvents.cs similarity index 100% rename from src/Squidex.Domain.Users/NoopUserEvents.cs rename to backend/src/Squidex.Domain.Users/NoopUserEvents.cs diff --git a/backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs b/backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs new file mode 100644 index 000000000..accc35925 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/PwnedPasswordValidator.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using SharpPwned.NET; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Users +{ + public sealed class PwnedPasswordValidator : IPasswordValidator + { + private const string ErrorCode = "PwnedError"; + private const string ErrorText = "This password has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it!"; + private static readonly IdentityResult Error = IdentityResult.Failed(new IdentityError { Code = ErrorCode, Description = ErrorText }); + + private readonly HaveIBeenPwnedRestClient client = new HaveIBeenPwnedRestClient(); + private readonly ISemanticLog log; + + public PwnedPasswordValidator(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public async Task ValidateAsync(UserManager manager, IdentityUser user, string password) + { + try + { + var isBreached = await client.IsPasswordPwned(password); + + if (isBreached) + { + return Error; + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("operation", "CheckPasswordPwned") + .WriteProperty("status", "Failed")); + } + + return IdentityResult.Success; + } + } +} diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj new file mode 100644 index 000000000..617da32a7 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -0,0 +1,31 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs b/backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs similarity index 100% rename from src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs rename to backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs diff --git a/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs new file mode 100644 index 000000000..251aa1420 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -0,0 +1,286 @@ +// ========================================================================== +// 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 System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Identity; + +namespace Squidex.Domain.Users +{ + public static class UserManagerExtensions + { + public static async Task GetUserWithClaimsAsync(this UserManager userManager, ClaimsPrincipal principal) + { + if (principal == null) + { + return null; + } + + var user = await userManager.FindByIdWithClaimsAsync(userManager.GetUserId(principal)); + + return user; + } + + public static async Task ResolveUserAsync(this UserManager userManager, IdentityUser user) + { + if (user == null) + { + return null; + } + + var claims = await userManager.GetClaimsAsync(user); + + return new UserWithClaims(user, claims); + } + + public static async Task FindByIdWithClaimsAsync(this UserManager userManager, string id) + { + if (id == null) + { + return null; + } + + var user = await userManager.FindByIdAsync(id); + + return await userManager.ResolveUserAsync(user); + } + + public static async Task FindByEmailWithClaimsAsyncAsync(this UserManager userManager, string email) + { + if (email == null) + { + return null; + } + + var user = await userManager.FindByEmailAsync(email); + + return await userManager.ResolveUserAsync(user); + } + + public static async Task FindByLoginWithClaimsAsync(this UserManager userManager, string loginProvider, string providerKey) + { + if (loginProvider == null || providerKey == null) + { + return null; + } + + var user = await userManager.FindByLoginAsync(loginProvider, providerKey); + + return await userManager.ResolveUserAsync(user); + } + + public static Task CountByEmailAsync(this UserManager userManager, string? email = null) + { + var count = QueryUsers(userManager, email).LongCount(); + + return Task.FromResult(count); + } + + public static async Task> QueryByIdsAync(this UserManager userManager, string[] ids) + { + var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); + + var result = await userManager.ResolveUsersAsync(users); + + return result.ToList(); + } + + public static async Task> QueryByEmailAsync(this UserManager userManager, string? email = null, int take = 10, int skip = 0) + { + var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); + + var result = await userManager.ResolveUsersAsync(users); + + return result.ToList(); + } + + public static Task ResolveUsersAsync(this UserManager userManager, IEnumerable users) + { + return Task.WhenAll(users.Select(async user => + { + return (await userManager.ResolveUserAsync(user))!; + })); + } + + public static IQueryable QueryUsers(UserManager userManager, string? email = null) + { + var result = userManager.Users; + + if (!string.IsNullOrWhiteSpace(email)) + { + var normalizedEmail = userManager.NormalizeEmail(email); + + result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); + } + + return result; + } + + public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, UserValues values) + { + var user = factory.Create(values.Email); + + try + { + await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); + + var claims = values.ToClaims(true); + + if (claims.Count > 0) + { + await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user."); + } + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user."); + } + } + catch + { + await userManager.DeleteAsync(user); + + throw; + } + + return (await userManager.ResolveUserAsync(user))!; + } + + public static async Task UpdateAsync(this UserManager userManager, string id, UserValues values) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await UpdateAsync(userManager, user, values); + + return (await userManager.ResolveUserAsync(user))!; + } + + public static Task GenerateClientSecretAsync(this UserManager userManager, IdentityUser user) + { + var claims = new List { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) }; + + return userManager.SyncClaimsAsync(user, claims); + } + + public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) + { + try + { + await userManager.UpdateAsync(user, values); + + return IdentityResult.Success; + } + catch (ValidationException ex) + { + return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray()); + } + } + + public static async Task UpdateAsync(this UserManager userManager, IdentityUser user, UserValues values) + { + if (user == null) + { + throw new DomainObjectNotFoundException("Id", typeof(IdentityUser)); + } + + if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) + { + await DoChecked(() => userManager.SetEmailAsync(user, values.Email), "Cannot update email."); + await DoChecked(() => userManager.SetUserNameAsync(user, values.Email), "Cannot update email."); + } + + await DoChecked(() => userManager.SyncClaimsAsync(user, values.ToClaims(false)), "Cannot update user."); + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot replace password."); + await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password."); + } + } + + public static async Task LockAsync(this UserManager userManager, string id) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user."); + + return (await userManager.ResolveUserAsync(user))!; + } + + public static async Task UnlockAsync(this UserManager userManager, string id) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); + + return (await userManager.ResolveUserAsync(user))!; + } + + private static async Task DoChecked(Func> action, string message) + { + var result = await action(); + + if (!result.Succeeded) + { + throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray()); + } + } + + public static async Task SyncClaimsAsync(this UserManager userManager, IdentityUser user, List claims) + { + if (claims.Any()) + { + var oldClaims = await userManager.GetClaimsAsync(user); + + var oldClaimsToRemove = new List(); + + foreach (var oldClaim in oldClaims) + { + if (claims.Any(x => x.Type == oldClaim.Type)) + { + oldClaimsToRemove.Add(oldClaim); + } + } + + if (oldClaimsToRemove.Count > 0) + { + var result = await userManager.RemoveClaimsAsync(user, oldClaimsToRemove); + + if (!result.Succeeded) + { + return result; + } + } + + return await userManager.AddClaimsAsync(user, claims.Where(x => !string.IsNullOrWhiteSpace(x.Value))); + } + + return IdentityResult.Success; + } + } +} diff --git a/src/Squidex.Domain.Users/UserValues.cs b/backend/src/Squidex.Domain.Users/UserValues.cs similarity index 100% rename from src/Squidex.Domain.Users/UserValues.cs rename to backend/src/Squidex.Domain.Users/UserValues.cs diff --git a/backend/src/Squidex.Domain.Users/UserWithClaims.cs b/backend/src/Squidex.Domain.Users/UserWithClaims.cs new file mode 100644 index 000000000..17f6f6bad --- /dev/null +++ b/backend/src/Squidex.Domain.Users/UserWithClaims.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class UserWithClaims : IUser + { + public IdentityUser Identity { get; } + + public List Claims { get; } + + public string Id + { + get { return Identity.Id; } + } + + public string Email + { + get { return Identity.Email; } + } + + public bool IsLocked + { + get { return Identity.LockoutEnd > DateTime.Now.ToUniversalTime(); } + } + + IReadOnlyList IUser.Claims + { + get { return Claims; } + } + + public UserWithClaims(IdentityUser user, IEnumerable claims) + { + Guard.NotNull(user); + Guard.NotNull(claims); + + Identity = user; + + Claims = claims.ToList(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs new file mode 100644 index 000000000..58cdb0db9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; + +namespace Squidex.Infrastructure.Assets +{ + public class AzureBlobAssetStore : IAssetStore, IInitializable + { + private readonly string containerName; + private readonly string connectionString; + private CloudBlobContainer blobContainer; + + public AzureBlobAssetStore(string connectionString, string containerName) + { + Guard.NotNullOrEmpty(containerName); + Guard.NotNullOrEmpty(connectionString); + + this.connectionString = connectionString; + this.containerName = containerName; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + var storageAccount = CloudStorageAccount.Parse(connectionString); + + var blobClient = storageAccount.CreateCloudBlobClient(); + var blobReference = blobClient.GetContainerReference(containerName); + + await blobReference.CreateIfNotExistsAsync(); + + blobContainer = blobReference; + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + if (blobContainer.Properties.PublicAccess != BlobContainerPublicAccessType.Blob) + { + var blob = blobContainer.GetBlockBlobReference(fileName); + + return blob.Uri.ToString(); + } + + return null; + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + try + { + var sourceBlob = blobContainer.GetBlockBlobReference(sourceFileName); + + var targetBlob = blobContainer.GetBlobReference(targetFileName); + + await targetBlob.StartCopyAsync(sourceBlob.Uri, null, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); + + while (targetBlob.CopyState.Status == CopyStatus.Pending) + { + ct.ThrowIfCancellationRequested(); + + await Task.Delay(50, ct); + await targetBlob.FetchAttributesAsync(null, null, null, ct); + } + + if (targetBlob.CopyState.Status != CopyStatus.Success) + { + throw new StorageException($"Copy of temporary file failed: {targetBlob.CopyState.Status}"); + } + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) + { + throw new AssetAlreadyExistsException(targetFileName); + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + var blob = blobContainer.GetBlockBlobReference(fileName); + + await blob.DownloadToStreamAsync(stream, null, null, null, ct); + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + var tempBlob = blobContainer.GetBlockBlobReference(fileName); + + await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); + } + catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + var blob = blobContainer.GetBlockBlobReference(fileName); + + return blob.DeleteIfExistsAsync(); + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs b/backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs rename to backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs rename to backend/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs rename to backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs similarity index 100% rename from src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs rename to backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs new file mode 100644 index 000000000..d40d3b7d5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Newtonsoft.Json; +using Index = Microsoft.Azure.Documents.Index; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed partial class CosmosDbEventStore : DisposableObjectBase, IEventStore, IInitializable + { + private readonly DocumentClient documentClient; + private readonly Uri collectionUri; + private readonly Uri databaseUri; + private readonly string masterKey; + private readonly string databaseId; + private readonly JsonSerializerSettings serializerSettings; + + public JsonSerializerSettings SerializerSettings + { + get { return serializerSettings; } + } + + public string DatabaseId + { + get { return databaseId; } + } + + public string MasterKey + { + get { return masterKey; } + } + + public Uri ServiceUri + { + get { return documentClient.ServiceEndpoint; } + } + + public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, JsonSerializerSettings serializerSettings) + { + Guard.NotNull(documentClient); + Guard.NotNull(serializerSettings); + Guard.NotNullOrEmpty(masterKey); + Guard.NotNullOrEmpty(database); + + this.documentClient = documentClient; + + databaseUri = UriFactory.CreateDatabaseUri(database); + databaseId = database; + + collectionUri = UriFactory.CreateDocumentCollectionUri(database, Constants.Collection); + + this.masterKey = masterKey; + + this.serializerSettings = serializerSettings; + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + documentClient.Dispose(); + } + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseId }); + + await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, + new DocumentCollection + { + PartitionKey = new PartitionKeyDefinition + { + Paths = new Collection + { + "/PartitionId" + } + }, + Id = Constants.LeaseCollection + }); + + await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, + new DocumentCollection + { + PartitionKey = new PartitionKeyDefinition + { + Paths = new Collection + { + "/eventStream" + } + }, + IndexingPolicy = new IndexingPolicy + { + IncludedPaths = new Collection + { + new IncludedPath + { + Path = "/*", + Indexes = new Collection + { + Index.Range(DataType.Number), + Index.Range(DataType.String) + } + } + } + }, + UniqueKeyPolicy = new UniqueKeyPolicy + { + UniqueKeys = new Collection + { + new UniqueKey + { + Paths = new Collection + { + "/eventStream", + "/eventStreamOffset" + } + } + } + }, + Id = Constants.Collection + }, + new RequestOptions + { + PartitionKey = new PartitionKey("/eventStream") + }); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs new file mode 100644 index 000000000..c0451bff2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public delegate bool EventPredicate(EventData data); + + public partial class CosmosDbEventStore : IEventStore, IInitializable + { + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(subscriber); + + ThrowIfDisposed(); + + return new CosmosDbSubscription(this, subscriber, streamFilter, position); + } + + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property); + + ThrowIfDisposed(); + + return TaskHelper.Done; + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName); + + ThrowIfDisposed(); + + using (Profiler.TraceMethod()) + { + var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize); + + var result = new List(); + + await documentClient.QueryAsync(collectionUri, query, commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (eventStreamOffset >= streamPosition) + { + var eventData = @event.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); + } + } + + return TaskHelper.Done; + }); + + return result; + } + } + + public Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + Guard.NotNullOrEmpty(property); + Guard.NotNull(value); + + ThrowIfDisposed(); + + StreamPosition lastPosition = position; + + var filterDefinition = FilterBuilder.CreateByProperty(property, value, lastPosition); + var filterExpression = FilterBuilder.CreateExpression(property, value); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + public Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + + ThrowIfDisposed(); + + StreamPosition lastPosition = position; + + var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition); + var filterExpression = FilterBuilder.CreateExpression(null, null); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + private async Task QueryAsync(Func callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default) + { + using (Profiler.TraceMethod()) + { + await documentClient.QueryAsync(collectionUri, query, async commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = @event.ToEventData(); + + if (filterExpression(eventData)) + { + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + } + } + + commitOffset++; + } + }, ct); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs new file mode 100644 index 000000000..52e848345 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using NodaTime; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class CosmosDbEventStore + { + private const int MaxWriteAttempts = 20; + private const int MaxCommitSize = 10; + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName); + + ThrowIfDisposed(); + + var query = FilterBuilder.AllIds(streamName); + + return documentClient.QueryAsync(collectionUri, query, commit => + { + var documentUri = UriFactory.CreateDocumentUri(databaseId, Constants.Collection, commit.Id.ToString()); + + return documentClient.DeleteDocumentAsync(documentUri); + }); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events); + } + + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.NotEmpty(commitId); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); + + ThrowIfDisposed(); + + using (Profiler.TraceMethod()) + { + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + + for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + { + try + { + await documentClient.CreateDocumentAsync(collectionUri, commit); + + return; + } + catch (DocumentClientException ex) + { + if (ex.StatusCode == HttpStatusCode.Conflict) + { + currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + if (attempt < MaxWriteAttempts) + { + expectedVersion = currentVersion; + } + else + { + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); + } + } + else + { + throw; + } + } + } + } + } + + private async Task GetEventStreamOffsetAsync(string streamName) + { + var query = + documentClient.CreateDocumentQuery(collectionUri, + FilterBuilder.LastPosition(streamName)); + + var document = await query.FirstOrDefaultAsync(); + + if (document != null) + { + return document.EventStreamOffset + document.EventsCount; + } + + return EtagVersion.Empty; + } + + private static CosmosDbEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + var commitEvents = new CosmosDbEvent[events.Count]; + + var i = 0; + + foreach (var e in events) + { + var mongoEvent = CosmosDbEvent.FromEventData(e); + + commitEvents[i++] = mongoEvent; + } + + var mongoCommit = new CosmosDbEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeTicks() + }; + + return mongoCommit; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs new file mode 100644 index 000000000..65b0d0965 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs @@ -0,0 +1,151 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing; +using Newtonsoft.Json; +using Squidex.Infrastructure.Tasks; +using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder; +using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo; +using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver + { + private readonly TaskCompletionSource processorStopRequested = new TaskCompletionSource(); + private readonly Task processorTask; + private readonly CosmosDbEventStore store; + private readonly Regex regex; + private readonly string? hostName; + private readonly IEventSubscriber subscriber; + + public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string? streamFilter, string? position = null) + { + this.store = store; + + var fromBeginning = string.IsNullOrWhiteSpace(position); + + if (fromBeginning) + { + hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; + } + else + { + hostName = position; + } + + if (!StreamFilter.IsAll(streamFilter)) + { + regex = new Regex(streamFilter); + } + + this.subscriber = subscriber; + + processorTask = Task.Run(async () => + { + try + { + Collection CreateCollection(string name) + { + var collection = new Collection(); + + collection.CollectionName = name; + collection.DatabaseName = store.DatabaseId; + collection.MasterKey = store.MasterKey; + collection.Uri = store.ServiceUri; + + return collection; + } + + var processor = + await new Builder() + .WithFeedCollection(CreateCollection(Constants.Collection)) + .WithLeaseCollection(CreateCollection(Constants.LeaseCollection)) + .WithHostName(hostName) + .WithProcessorOptions(new Options { StartFromBeginning = fromBeginning, LeasePrefix = hostName }) + .WithObserverFactory(this) + .BuildAsync(); + + await processor.StartAsync(); + await processorStopRequested.Task; + await processor.StopAsync(); + } + catch (Exception ex) + { + await subscriber.OnErrorAsync(this, ex); + } + }); + } + + public IChangeFeedObserver CreateObserver() + { + return this; + } + + public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason) + { + if (reason == ChangeFeedObserverCloseReason.ObserverError) + { + await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed.")); + } + } + + public Task OpenAsync(IChangeFeedObserverContext context) + { + return TaskHelper.Done; + } + + public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) + { + if (!processorStopRequested.Task.IsCompleted) + { + foreach (var document in docs) + { + if (!processorStopRequested.Task.IsCompleted) + { + var streamName = document.GetPropertyValue("eventStream"); + + if (regex == null || regex.IsMatch(streamName)) + { + var commit = JsonConvert.DeserializeObject(document.ToString(), store.SerializerSettings); + + var eventStreamOffset = (int)commit.EventStreamOffset; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + var eventData = @event.ToEventData(); + + await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName ?? "None", eventStreamOffset, eventData)); + } + } + } + } + } + } + + public void WakeUp() + { + } + + public Task StopAsync() + { + processorStopRequested.SetResult(true); + + return processorTask; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs new file mode 100644 index 000000000..301be5b47 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs @@ -0,0 +1,156 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Azure.Documents; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterBuilder + { + public static SqlQuerySpec AllIds(string streamName) + { + var query = + $"SELECT TOP 1 " + + $" e.id," + + $" e.eventsCount " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"ORDER BY e.eventStreamOffset DESC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec LastPosition(string streamName) + { + var query = + $"SELECT TOP 1 " + + $" e.eventStreamOffset," + + $" e.eventsCount " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"ORDER BY e.eventStreamOffset DESC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec ByStreamName(string streamName, long streamPosition = 0) + { + var query = + $"SELECT * " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"AND e.eventStreamOffset >= @position " + + $"ORDER BY e.eventStreamOffset ASC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName), + new SqlParameter("@position", streamPosition) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec CreateByProperty(string property, object value, StreamPosition streamPosition) + { + var filters = new List(); + + var parameters = new SqlParameterCollection(); + + filters.ForPosition(parameters, streamPosition); + filters.ForProperty(parameters, property, value); + + return BuildQuery(filters, parameters); + } + + public static SqlQuerySpec CreateByFilter(string? streamFilter, StreamPosition streamPosition) + { + var filters = new List(); + + var parameters = new SqlParameterCollection(); + + filters.ForPosition(parameters, streamPosition); + filters.ForRegex(parameters, streamFilter); + + return BuildQuery(filters, parameters); + } + + private static SqlQuerySpec BuildQuery(IEnumerable filters, SqlParameterCollection parameters) + { + var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp"; + + return new SqlQuerySpec(query, parameters); + } + + private static void ForProperty(this ICollection filters, SqlParameterCollection parameters, string property, object value) + { + filters.Add($"ARRAY_CONTAINS(e.events, {{ \"header\": {{ \"{property}\": @value }} }}, true)"); + + parameters.Add(new SqlParameter("@value", value)); + } + + private static void ForRegex(this ICollection filters, SqlParameterCollection parameters, string? streamFilter) + { + if (!StreamFilter.IsAll(streamFilter)) + { + if (streamFilter.Contains("^")) + { + filters.Add($"STARTSWITH(e.eventStream, @filter)"); + } + else + { + filters.Add($"e.eventStream = @filter"); + } + + parameters.Add(new SqlParameter("@filter", streamFilter)); + } + } + + private static void ForPosition(this ICollection filters, SqlParameterCollection parameters, StreamPosition streamPosition) + { + if (streamPosition.IsEndOfCommit) + { + filters.Add($"e.timestamp > @time"); + } + else + { + filters.Add($"e.timestamp >= @time"); + } + + parameters.Add(new SqlParameter("@time", streamPosition.Timestamp)); + } + + public static EventPredicate CreateExpression(string? property, object? value) + { + if (!string.IsNullOrWhiteSpace(property)) + { + var jsonValue = JsonValue.Create(value); + + return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); + } + else + { + return x => true; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs new file mode 100644 index 000000000..77dc23c86 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Microsoft.Azure.Documents.Linq; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterExtensions + { + public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + if (documentQuery.HasMoreResults) + { + var results = await documentQuery.ExecuteNextAsync(ct); + + return results.FirstOrDefault(); + } + } + + return default!; + } + + public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) + { + var query = documentClient.CreateDocumentQuery(collectionUri, querySpec); + + return query.QueryAsync(handler, ct); + } + + public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + while (documentQuery.HasMoreResults && !ct.IsCancellationRequested) + { + var items = await documentQuery.ExecuteNextAsync(ct); + + foreach (var item in items) + { + await handler(item); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs new file mode 100644 index 000000000..d783f0c41 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class StreamPosition + { + public long Timestamp { get; } + + public long CommitOffset { get; } + + public long CommitSize { get; } + + public bool IsEndOfCommit + { + get { return CommitOffset == CommitSize - 1; } + } + + public StreamPosition(long timestamp, long commitOffset, long commitSize) + { + Timestamp = timestamp; + + CommitOffset = commitOffset; + CommitSize = commitSize; + } + + public static implicit operator string(StreamPosition position) + { + var parts = new object[] + { + position.Timestamp, + position.CommitOffset, + position.CommitSize + }; + + return string.Join("-", parts); + } + + public static implicit operator StreamPosition(string? position) + { + if (!string.IsNullOrWhiteSpace(position)) + { + var parts = position.Split('-'); + + return new StreamPosition(long.Parse(parts[0]), long.Parse(parts[1]), long.Parse(parts[2])); + } + + return new StreamPosition(0, -1, -1); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj new file mode 100644 index 000000000..00953e0bb --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -0,0 +1,24 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs new file mode 100644 index 000000000..983c3b83b --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GetEventStoreHealthCheck : IHealthCheck + { + private readonly IEventStoreConnection connection; + + public GetEventStoreHealthCheck(IEventStoreConnection connection) + { + Guard.NotNull(connection); + + this.connection = connection; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + await connection.ReadEventAsync("test", 1, false); + + return HealthCheckResult.Healthy("Application must query data from EventStore."); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs new file mode 100644 index 000000000..99dfe137b --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// 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 System.Text; +using EventStore.ClientAPI; +using Squidex.Infrastructure.Json; +using EventStoreData = EventStore.ClientAPI.EventData; + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class Formatter + { + private static readonly HashSet PrivateHeaders = new HashSet { "$v", "$p", "$c", "$causedBy" }; + + public static StoredEvent Read(ResolvedEvent resolvedEvent, string? prefix, IJsonSerializer serializer) + { + var @event = resolvedEvent.Event; + + var eventPayload = Encoding.UTF8.GetString(@event.Data); + var eventHeaders = GetHeaders(serializer, @event); + + var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); + + var streamName = GetStreamName(prefix, @event); + + return new StoredEvent( + streamName, + resolvedEvent.OriginalEventNumber.ToString(), + resolvedEvent.Event.EventNumber, + eventData); + } + + private static string GetStreamName(string? prefix, RecordedEvent @event) + { + var streamName = @event.EventStreamId; + + if (prefix != null && streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + streamName = streamName.Substring(prefix.Length + 1); + } + + return streamName; + } + + private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event) + { + var headersJson = Encoding.UTF8.GetString(@event.Metadata); + var headers = serializer.Deserialize(headersJson); + + foreach (var key in headers.Keys.ToList()) + { + if (PrivateHeaders.Contains(key)) + { + headers.Remove(key); + } + } + + return headers; + } + + public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) + { + var payload = Encoding.UTF8.GetBytes(eventData.Payload); + + var headersJson = serializer.Serialize(eventData.Headers); + var headersBytes = Encoding.UTF8.GetBytes(headersJson); + + return new EventStoreData(Guid.NewGuid(), eventData.Type, true, payload, headersBytes); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs new file mode 100644 index 000000000..f9525cb6a --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -0,0 +1,224 @@ +// ========================================================================== +// 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 System.Threading; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class GetEventStore : IEventStore, IInitializable + { + private const int WritePageSize = 500; + private const int ReadPageSize = 500; + private readonly IEventStoreConnection connection; + private readonly IJsonSerializer serializer; + private readonly string prefix; + private readonly ProjectionClient projectionClient; + + public GetEventStore(IEventStoreConnection connection, IJsonSerializer serializer, string prefix, string projectionHost) + { + Guard.NotNull(connection); + Guard.NotNull(serializer); + + this.connection = connection; + this.serializer = serializer; + + this.prefix = prefix.Trim(' ', '-').WithFallback("squidex"); + + projectionClient = new ProjectionClient(connection, prefix, projectionHost); + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + await connection.ConnectAsync(); + } + catch (Exception ex) + { + throw new ConfigurationException("Cannot connect to event store.", ex); + } + + await projectionClient.ConnectAsync(); + } + + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(streamFilter); + + return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter); + } + + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property); + + return projectionClient.CreateProjectionAsync(property, string.Empty); + } + + public async Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + Guard.NotNullOrEmpty(property); + Guard.NotNull(value); + + using (Profiler.TraceMethod()) + { + var streamName = await projectionClient.CreateProjectionAsync(property, value); + + var sliceStart = projectionClient.ParsePosition(position); + + await QueryAsync(callback, streamName, sliceStart, ct); + } + } + + public async Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + + using (Profiler.TraceMethod()) + { + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); + + var sliceStart = projectionClient.ParsePosition(position); + + await QueryAsync(callback, streamName, sliceStart, ct); + } + } + + private async Task QueryAsync(Func callback, string streamName, long sliceStart, CancellationToken ct = default) + { + StreamEventsSlice currentSlice; + do + { + currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true); + + if (currentSlice.Status == SliceReadStatus.Success) + { + sliceStart = currentSlice.NextEventNumber; + + foreach (var resolved in currentSlice.Events) + { + var storedEvent = Formatter.Read(resolved, prefix, serializer); + + await callback(storedEvent); + } + } + } + while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested); + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName); + + using (Profiler.TraceMethod()) + { + var result = new List(); + + var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start; + + StreamEventsSlice currentSlice; + do + { + currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true); + + if (currentSlice.Status == SliceReadStatus.Success) + { + sliceStart = currentSlice.NextEventNumber; + + foreach (var resolved in currentSlice.Events) + { + var storedEvent = Formatter.Read(resolved, prefix, serializer); + + result.Add(storedEvent); + } + } + } + while (!currentSlice.IsEndOfStream); + + return result; + } + } + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName); + + return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); + } + + public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.GreaterEquals(expectedVersion, -1); + + return AppendEventsInternalAsync(streamName, expectedVersion, events); + } + + private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection events) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + + using (Profiler.TraceMethod(nameof(AppendAsync))) + { + if (events.Count == 0) + { + return; + } + + try + { + var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList(); + + if (eventsToSave.Count < WritePageSize) + { + await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave); + } + else + { + using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion)) + { + for (var p = 0; p < eventsToSave.Count; p += WritePageSize) + { + await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); + } + + await transaction.CommitAsync(); + } + } + } + catch (WrongExpectedVersionException ex) + { + throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion); + } + } + } + + private static int ParseVersion(string message) + { + return int.Parse(message.Substring(message.LastIndexOf(':') + 1)); + } + + private string GetStreamName(string streamName) + { + return $"{prefix}-{streamName}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs new file mode 100644 index 000000000..3fa20e8ae --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class GetEventStoreSubscription : IEventSubscription + { + private readonly IEventStoreConnection connection; + private readonly IEventSubscriber subscriber; + private readonly IJsonSerializer serializer; + private readonly string? prefix; + private readonly EventStoreCatchUpSubscription subscription; + private readonly long? position; + + public GetEventStoreSubscription( + IEventStoreConnection connection, + IEventSubscriber subscriber, + IJsonSerializer serializer, + ProjectionClient projectionClient, + string? position, + string? prefix, + string? streamFilter) + { + this.connection = connection; + + this.position = projectionClient.ParsePositionOrNull(position); + this.prefix = prefix; + + var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; + + this.serializer = serializer; + this.subscriber = subscriber; + + subscription = SubscribeToStream(streamName); + } + + public Task StopAsync() + { + subscription.Stop(); + + return TaskHelper.Done; + } + + public void WakeUp() + { + } + + private EventStoreCatchUpSubscription SubscribeToStream(string streamName) + { + var settings = CatchUpSubscriptionSettings.Default; + + return connection.SubscribeToStreamFrom(streamName, position, settings, + (s, e) => + { + var storedEvent = Formatter.Read(e, prefix, serializer); + + subscriber.OnEventAsync(this, storedEvent).Wait(); + }, null, + (s, reason, ex) => + { + if (reason != SubscriptionDropReason.ConnectionClosed && + reason != SubscriptionDropReason.UserInitiated) + { + ex ??= new ConnectionClosedException($"Subscription closed with reason {reason}."); + + subscriber.OnErrorAsync(this, ex); + } + }); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs new file mode 100644 index 000000000..3136d7f0d --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; +using EventStore.ClientAPI.Projections; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class ProjectionClient + { + private readonly ConcurrentDictionary projections = new ConcurrentDictionary(); + private readonly IEventStoreConnection connection; + private readonly string prefix; + private readonly string projectionHost; + private ProjectionsManager projectionsManager; + + public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost) + { + this.connection = connection; + + this.prefix = prefix; + this.projectionHost = projectionHost; + } + + private string CreateFilterProjectionName(string filter) + { + return $"by-{prefix.Slugify()}-{filter.Slugify()}"; + } + + private string CreatePropertyProjectionName(string property) + { + return $"by-{prefix.Slugify()}-{property.Slugify()}-property"; + } + + public async Task CreateProjectionAsync(string property, object value) + { + var name = CreatePropertyProjectionName(property); + + var query = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && e.metadata.{property}) {{ + linkTo('{name}-' + e.metadata.{property}, e); + }} + }} + }});"; + + await CreateProjectionAsync(name, query); + + return $"{name}-{value}"; + } + + public async Task CreateProjectionAsync(string? streamFilter = null) + { + streamFilter ??= ".*"; + + var name = CreateFilterProjectionName(streamFilter); + + var query = + $@"fromAll() + .when({{ + $any: function (s, e) {{ + if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ + linkTo('{name}', e); + }} + }} + }});"; + + await CreateProjectionAsync(name, query); + + return name; + } + + private async Task CreateProjectionAsync(string name, string query) + { + if (projections.TryAdd(name, true)) + { + try + { + var credentials = connection.Settings.DefaultUserCredentials; + + await projectionsManager.CreateContinuousAsync(name, query, credentials); + } + catch (Exception ex) + { + if (!ex.Is()) + { + throw; + } + } + } + } + + public async Task ConnectAsync() + { + var addressParts = projectionHost.Split(':'); + + if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) + { + port = 2113; + } + + var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); + var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); + + projectionsManager = + new ProjectionsManager( + connection.Settings.Log, endpoint, + connection.Settings.OperationTimeout); + try + { + await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials); + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); + } + } + + public long? ParsePositionOrNull(string? position) + { + return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; + } + + public long ParsePosition(string? position) + { + return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj new file mode 100644 index 000000000..1b7f280b3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -0,0 +1,26 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs new file mode 100644 index 000000000..947c7c222 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -0,0 +1,112 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Google; +using Google.Cloud.Storage.V1; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class GoogleCloudAssetStore : IAssetStore, IInitializable + { + private static readonly UploadObjectOptions IfNotExists = new UploadObjectOptions { IfGenerationMatch = 0 }; + private static readonly CopyObjectOptions IfNotExistsCopy = new CopyObjectOptions { IfGenerationMatch = 0 }; + private readonly string bucketName; + private StorageClient storageClient; + + public GoogleCloudAssetStore(string bucketName) + { + Guard.NotNullOrEmpty(bucketName); + + this.bucketName = bucketName; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + storageClient = StorageClient.Create(); + + await storageClient.GetBucketAsync(bucketName, cancellationToken: ct); + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot connect to google cloud bucket '${bucketName}'.", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + try + { + await storageClient.CopyObjectAsync(bucketName, sourceFileName, bucketName, targetFileName, IfNotExistsCopy, ct); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) + { + throw new AssetAlreadyExistsException(targetFileName); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + await storageClient.DownloadObjectAsync(bucketName, fileName, stream, cancellationToken: ct); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + + try + { + await storageClient.UploadObjectAsync(bucketName, fileName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public async Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + try + { + await storageClient.DeleteObjectAsync(bucketName, fileName); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) + { + return; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj b/backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj new file mode 100644 index 000000000..fc3ff0be8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj @@ -0,0 +1,27 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs new file mode 100644 index 000000000..6c585da51 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable + { + private const int BufferSize = 81920; + private readonly IGridFSBucket bucket; + + public MongoGridFsAssetStore(IGridFSBucket bucket) + { + Guard.NotNull(bucket); + + this.bucket = bucket; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + await bucket.Database.ListCollectionsAsync(cancellationToken: ct); + } + catch (MongoException ex) + { + throw new ConfigurationException($"Cannot connect to Mongo GridFS bucket '${bucket.Options.BucketName}'.", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(targetFileName); + + try + { + var sourceName = GetFileName(sourceFileName, nameof(sourceFileName)); + + using (var readStream = await bucket.OpenDownloadStreamAsync(sourceFileName, cancellationToken: ct)) + { + await UploadAsync(targetFileName, readStream, false, ct); + } + } + catch (GridFSFileNotFoundException ex) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNull(stream); + + try + { + var name = GetFileName(fileName, nameof(fileName)); + + using (var readStream = await bucket.OpenDownloadStreamAsync(name, cancellationToken: ct)) + { + await readStream.CopyToAsync(stream, BufferSize, ct); + } + } + catch (GridFSFileNotFoundException ex) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNull(stream); + + try + { + var name = GetFileName(fileName, nameof(fileName)); + + if (overwrite) + { + await DeleteAsync(fileName); + } + + await bucket.UploadFromStreamAsync(fileName, fileName, stream, cancellationToken: ct); + } + catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + throw new AssetAlreadyExistsException(fileName); + } + catch (MongoBulkWriteException ex) when (ex.WriteErrors.Any(x => x.Category == ServerErrorCategory.DuplicateKey)) + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public async Task DeleteAsync(string fileName) + { + try + { + var name = GetFileName(fileName, nameof(fileName)); + + await bucket.DeleteAsync(name); + } + catch (GridFSFileNotFoundException) + { + return; + } + } + + private static string GetFileName(string fileName, string parameterName) + { + Guard.NotNullOrEmpty(fileName); + + return fileName; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs new file mode 100644 index 000000000..1f200b961 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class MongoDBHealthCheck : IHealthCheck + { + private readonly IMongoDatabase mongoDatabase; + + public MongoDBHealthCheck(IMongoDatabase mongoDatabase) + { + Guard.NotNull(mongoDatabase); + + this.mongoDatabase = mongoDatabase; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); + + var result = await collectionNames.AnyAsync(cancellationToken); + + var status = result ? + HealthStatus.Healthy : + HealthStatus.Unhealthy; + + return new HealthCheckResult(status, "Application must query data from MongoDB"); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs rename to backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs rename to backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs new file mode 100644 index 000000000..b8ed11d0c --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class MongoEventStore : MongoRepositoryBase, IEventStore + { + private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); + private static readonly FieldDefinition EventsCountField = Fields.Build(x => x.EventsCount); + private static readonly FieldDefinition EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); + private static readonly FieldDefinition EventStreamField = Fields.Build(x => x.EventStream); + private readonly IEventNotifier notifier; + + public IMongoCollection RawCollection + { + get { return Database.GetCollection(CollectionName()); } + } + + public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) + : base(database) + { + Guard.NotNull(notifier); + + this.notifier = notifier; + } + + protected override string CollectionName() + { + return "Events"; + } + + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { ReadPreference = ReadPreference.Primary, WriteConcern = WriteConcern.WMajority }; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.Timestamp) + .Ascending(x => x.EventStream)), + new CreateIndexModel( + Index + .Ascending(x => x.EventStream) + .Descending(x => x.EventStreamOffset), + new CreateIndexOptions + { + Unique = true + }) + }, ct); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs new file mode 100644 index 000000000..757c19348 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -0,0 +1,210 @@ +// ========================================================================== +// 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.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using EventFilter = MongoDB.Driver.FilterDefinition; + +namespace Squidex.Infrastructure.EventSourcing +{ + public delegate bool EventPredicate(EventData data); + + public partial class MongoEventStore : MongoRepositoryBase, IEventStore + { + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property); + + return Collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index.Ascending(CreateIndexPath(property)))); + } + + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(subscriber); + + return new PollingSubscription(this, subscriber, streamFilter, position); + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName); + + using (Profiler.TraceMethod()) + { + var commits = + await Collection.Find( + Filter.And( + Filter.Eq(EventStreamField, streamName), + Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize))) + .Sort(Sort.Ascending(TimestampField)).ToListAsync(); + + var result = new List(); + + foreach (var commit in commits) + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (eventStreamOffset >= streamPosition) + { + var eventData = @event.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); + } + } + } + + return result; + } + } + + public Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + Guard.NotNullOrEmpty(property); + Guard.NotNull(value); + + StreamPosition lastPosition = position; + + var filterDefinition = CreateFilter(property, value, lastPosition); + var filterExpression = CreateFilterExpression(property, value); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + public Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default) + { + Guard.NotNull(callback); + + StreamPosition lastPosition = position; + + var filterDefinition = CreateFilter(streamFilter, lastPosition); + var filterExpression = CreateFilterExpression(null, null); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + private async Task QueryAsync(Func callback, StreamPosition lastPosition, EventFilter filterDefinition, EventPredicate filterExpression, CancellationToken ct = default) + { + using (Profiler.TraceMethod()) + { + await Collection.Find(filterDefinition, options: Batching.Options).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = @event.ToEventData(); + + if (filterExpression(eventData)) + { + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + } + } + + commitOffset++; + } + }, ct); + } + } + + private static EventFilter CreateFilter(string property, object value, StreamPosition streamPosition) + { + var filters = new List(); + + AppendByPosition(streamPosition, filters); + AppendByProperty(property, value, filters); + + return Filter.And(filters); + } + + private static EventFilter CreateFilter(string? streamFilter, StreamPosition streamPosition) + { + var filters = new List(); + + AppendByPosition(streamPosition, filters); + AppendByStream(streamFilter, filters); + + return Filter.And(filters); + } + + private static void AppendByProperty(string property, object value, List filters) + { + filters.Add(Filter.Eq(CreateIndexPath(property), value)); + } + + private static void AppendByStream(string? streamFilter, List filters) + { + if (!StreamFilter.IsAll(streamFilter)) + { + if (streamFilter.Contains("^")) + { + filters.Add(Filter.Regex(EventStreamField, streamFilter)); + } + else + { + filters.Add(Filter.Eq(EventStreamField, streamFilter)); + } + } + } + + private static void AppendByPosition(StreamPosition streamPosition, List filters) + { + if (streamPosition.IsEndOfCommit) + { + filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp)); + } + else + { + filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp)); + } + } + + private static EventPredicate CreateFilterExpression(string? property, object? value) + { + if (!string.IsNullOrWhiteSpace(property)) + { + var jsonValue = JsonValue.Create(value); + + return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); + } + else + { + return x => true; + } + } + + private static string CreateIndexPath(string property) + { + return $"Events.Metadata.{property}"; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs new file mode 100644 index 000000000..5b50bbe90 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// 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.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class MongoEventStore + { + private const int MaxCommitSize = 10; + private const int MaxWriteAttempts = 20; + private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName); + + return Collection.DeleteManyAsync(x => x.EventStream == streamName); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events); + } + + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.NotEmpty(commitId); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); + Guard.GreaterEquals(expectedVersion, EtagVersion.Any); + + using (Profiler.TraceMethod()) + { + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + + for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + { + try + { + await Collection.InsertOneAsync(commit); + + notifier.NotifyEventsStored(streamName); + + return; + } + catch (MongoWriteException ex) + { + if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + { + currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion > EtagVersion.Any) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + if (attempt < MaxWriteAttempts) + { + expectedVersion = currentVersion; + } + else + { + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); + } + } + else + { + throw; + } + } + } + } + } + + private async Task GetEventStreamOffsetAsync(string streamName) + { + var document = + await Collection.Find(Filter.Eq(EventStreamField, streamName)) + .Project(Projection + .Include(EventStreamOffsetField) + .Include(EventsCountField)) + .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) + .FirstOrDefaultAsync(); + + if (document != null) + { + return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); + } + + return EtagVersion.Empty; + } + + private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + var commitEvents = new MongoEvent[events.Count]; + + var i = 0; + + foreach (var e in events) + { + var mongoEvent = MongoEvent.FromEventData(e); + + commitEvents[i++] = mongoEvent; + } + + var mongoCommit = new MongoEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = EmptyTimestamp + }; + + return mongoCommit; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs new file mode 100644 index 000000000..dcfb32884 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class StreamPosition + { + private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(946681200, 0); + + public BsonTimestamp Timestamp { get; } + + public long CommitOffset { get; } + + public long CommitSize { get; } + + public bool IsEndOfCommit + { + get { return CommitOffset == CommitSize - 1; } + } + + public StreamPosition(BsonTimestamp timestamp, long commitOffset, long commitSize) + { + Timestamp = timestamp; + + CommitOffset = commitOffset; + CommitSize = commitSize; + } + + public static implicit operator string(StreamPosition position) + { + var parts = new object[] + { + position.Timestamp.Timestamp, + position.Timestamp.Increment, + position.CommitOffset, + position.CommitSize + }; + + return string.Join("-", parts); + } + + public static implicit operator StreamPosition(string? position) + { + if (!string.IsNullOrWhiteSpace(position)) + { + var parts = position.Split('-'); + + return new StreamPosition(new BsonTimestamp(int.Parse(parts[0]), int.Parse(parts[1])), long.Parse(parts[2]), long.Parse(parts[3])); + } + + return new StreamPosition(EmptyTimestamp, -1, -1); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs rename to backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs rename to backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs new file mode 100644 index 000000000..671b2caba --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.MongoDb +{ + public static class BsonJsonConvention + { + private static volatile int isRegistered; + + public static void Register(JsonSerializer serializer) + { + if (Interlocked.Increment(ref isRegistered) == 1) + { + var pack = new ConventionPack(); + + pack.AddMemberMapConvention("JsonBson", memberMap => + { + var attributes = memberMap.MemberInfo.GetCustomAttributes(); + + if (attributes.OfType().Any()) + { + var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); + var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); + + memberMap.SetSerializer((IBsonSerializer)bsonSerializer!); + } + else if (memberMap.MemberType == typeof(JToken)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JObject)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JValue)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + }); + + ConventionRegistry.Register("json", pack, t => true); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs new file mode 100644 index 000000000..87657127b --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader; +using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonReader : NewtonsoftJsonReader + { + private readonly IBsonReader bsonReader; + + public BsonJsonReader(IBsonReader bsonReader) + { + Guard.NotNull(bsonReader); + + this.bsonReader = bsonReader; + } + + public override bool Read() + { + if (bsonReader.State == BsonReaderState.Initial || + bsonReader.State == BsonReaderState.ScopeDocument || + bsonReader.State == BsonReaderState.Type) + { + bsonReader.ReadBsonType(); + } + + if (bsonReader.State == BsonReaderState.Name) + { + SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson()); + } + else if (bsonReader.State == BsonReaderState.Value) + { + switch (bsonReader.CurrentBsonType) + { + case BsonType.Document: + SetToken(NewtonsoftJsonToken.StartObject); + bsonReader.ReadStartDocument(); + break; + case BsonType.Array: + SetToken(NewtonsoftJsonToken.StartArray); + bsonReader.ReadStartArray(); + break; + case BsonType.Undefined: + SetToken(NewtonsoftJsonToken.Undefined); + bsonReader.ReadUndefined(); + break; + case BsonType.Null: + SetToken(NewtonsoftJsonToken.Null); + bsonReader.ReadNull(); + break; + case BsonType.String: + SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString()); + break; + case BsonType.Binary: + SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes); + break; + case BsonType.Boolean: + SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean()); + break; + case BsonType.DateTime: + SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime()); + break; + case BsonType.Int32: + SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32()); + break; + case BsonType.Int64: + SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64()); + break; + case BsonType.Double: + SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble()); + break; + case BsonType.Decimal128: + SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128())); + break; + default: + throw new NotSupportedException(); + } + } + else if (bsonReader.State == BsonReaderState.EndOfDocument) + { + SetToken(NewtonsoftJsonToken.EndObject); + bsonReader.ReadEndDocument(); + } + else if (bsonReader.State == BsonReaderState.EndOfArray) + { + SetToken(NewtonsoftJsonToken.EndArray); + bsonReader.ReadEndArray(); + } + + if (bsonReader.State == BsonReaderState.Initial) + { + return true; + } + + return !bsonReader.IsAtEndOfFile(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs new file mode 100644 index 000000000..f5896212f --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonSerializer : ClassSerializerBase where T : class + { + private readonly JsonSerializer serializer; + + public BsonJsonSerializer(JsonSerializer serializer) + { + Guard.NotNull(serializer); + + this.serializer = serializer; + } + + public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bsonReader = context.Reader; + + if (bsonReader.GetCurrentBsonType() == BsonType.Null) + { + bsonReader.ReadNull(); + + return null; + } + else + { + var jsonReader = new BsonJsonReader(bsonReader); + + return serializer.Deserialize(jsonReader); + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) + { + var bsonWriter = context.Writer; + + if (value == null) + { + bsonWriter.WriteNull(); + } + else + { + var jsonWriter = new BsonJsonWriter(bsonWriter); + + serializer.Serialize(jsonWriter, value); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs new file mode 100644 index 000000000..22f52e559 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs @@ -0,0 +1,178 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Globalization; +using MongoDB.Bson.IO; +using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonWriter : NewtonsoftJSonWriter + { + private readonly IBsonWriter bsonWriter; + + public BsonJsonWriter(IBsonWriter bsonWriter) + { + Guard.NotNull(bsonWriter); + + this.bsonWriter = bsonWriter; + } + + public override void WritePropertyName(string name) + { + bsonWriter.WriteName(name.EscapeJson()); + } + + public override void WritePropertyName(string name, bool escape) + { + bsonWriter.WriteName(name.EscapeJson()); + } + + public override void WriteStartArray() + { + bsonWriter.WriteStartArray(); + } + + public override void WriteEndArray() + { + bsonWriter.WriteEndArray(); + } + + public override void WriteStartObject() + { + bsonWriter.WriteStartDocument(); + } + + public override void WriteEndObject() + { + bsonWriter.WriteEndDocument(); + } + + public override void WriteNull() + { + bsonWriter.WriteNull(); + } + + public override void WriteUndefined() + { + bsonWriter.WriteUndefined(); + } + + public override void WriteValue(string value) + { + bsonWriter.WriteString(value); + } + + public override void WriteValue(int value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(uint value) + { + bsonWriter.WriteInt32((int)value); + } + + public override void WriteValue(long value) + { + bsonWriter.WriteInt64(value); + } + + public override void WriteValue(ulong value) + { + bsonWriter.WriteInt64((long)value); + } + + public override void WriteValue(float value) + { + bsonWriter.WriteDouble(value); + } + + public override void WriteValue(double value) + { + bsonWriter.WriteDouble(value); + } + + public override void WriteValue(bool value) + { + bsonWriter.WriteBoolean(value); + } + + public override void WriteValue(short value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(ushort value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(char value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(byte value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(sbyte value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(decimal value) + { + bsonWriter.WriteDecimal128(value); + } + + public override void WriteValue(DateTime value) + { + bsonWriter.WriteString(value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + } + + public override void WriteValue(DateTimeOffset value) + { + if (value.Offset == TimeSpan.Zero) + { + bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + } + else + { + bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + } + } + + public override void WriteValue(byte[] value) + { + bsonWriter.WriteBytes(value); + } + + public override void WriteValue(TimeSpan value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(Guid value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(Uri value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void Flush() + { + bsonWriter.Flush(); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs new file mode 100644 index 000000000..ccdc54493 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class JTokenSerializer : ClassSerializerBase where T : JToken + { + public static readonly JTokenSerializer Instance = new JTokenSerializer(); + + public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bsonReader = context.Reader; + + if (bsonReader.GetCurrentBsonType() == BsonType.Null) + { + bsonReader.ReadNull(); + + return null; + } + else + { + var jsonReader = new BsonJsonReader(bsonReader); + + return (T)JToken.ReadFrom(jsonReader); + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) + { + var bsonWriter = context.Writer; + + if (value == null) + { + bsonWriter.WriteNull(); + } + else + { + var jsonWriter = new BsonJsonWriter(bsonWriter); + + value.WriteTo(jsonWriter); + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs new file mode 100644 index 000000000..dae2fc7af --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -0,0 +1,216 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.States; + +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + +namespace Squidex.Infrastructure.MongoDb +{ + public static class MongoExtensions + { + private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + + public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName) + { + var options = new ListCollectionNamesOptions + { + Filter = new BsonDocument("name", collectionName) + }; + + return (await database.ListCollectionNamesAsync(options)).Any(); + } + + public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document) + { + try + { + await collection.InsertOneAsync(document); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + return false; + } + + throw; + } + + return true; + } + + public static async Task TryDropOneAsync(this IMongoIndexManager indexes, string name) + { + try + { + await indexes.DropOneAsync(name); + } + catch + { + /* NOOP */ + } + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include) + { + return find.Project(Builders.Projection.Include(include)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2) + { + return find.Project(Builders.Projection.Include(include1).Include(include2)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2, + Expression> include3) + { + return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); + } + + public static IFindFluent Not(this IFindFluent find, + Expression> exclude) + { + return find.Project(Builders.Projection.Exclude(exclude)); + } + + public static IFindFluent Not(this IFindFluent find, + Expression> exclude1, + Expression> exclude2) + { + return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2)); + } + + public static IFindFluent Not(this IFindFluent find, + Expression> exclude1, + Expression> exclude2, + Expression> exclude3) + { + return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2).Exclude(exclude3)); + } + + public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, Func, UpdateDefinition> updater) where T : IVersionedEntity where TKey : notnull + { + try + { + var update = updater(Builders.Update.Set(x => x.Version, newVersion)); + + await collection.UpdateOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, update, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } + + public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, T doc) where T : IVersionedEntity where TKey : notnull + { + try + { + await collection.ReplaceOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, doc, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } + + public static async Task ForEachPipelineAsync(this IAsyncCursorSource source, Func processor, CancellationToken cancellationToken = default) + { + using (var cursor = await source.ToCursorAsync(cancellationToken)) + { + await cursor.ForEachPipelineAsync(processor, cancellationToken); + } + } + + public static async Task ForEachPipelineAsync(this IAsyncCursor source, Func processor, CancellationToken cancellationToken = default) + { + using (var selfToken = new CancellationTokenSource()) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(selfToken.Token, cancellationToken)) + { + var actionBlock = + new ActionBlock(async x => + { + if (!combined.IsCancellationRequested) + { + await processor(x); + } + }, + new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + BoundedCapacity = Batching.BufferSize + }); + try + { + await source.ForEachAsync(async i => + { + var t = source; + + if (!await actionBlock.SendAsync(i, combined.Token)) + { + selfToken.Cancel(); + } + }, combined.Token); + + actionBlock.Complete(); + } + catch (Exception ex) + { + ((IDataflowBlock)actionBlock).Fault(ex); + } + finally + { + await actionBlock.Completion; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs new file mode 100644 index 000000000..d452dab6d --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.MongoDb +{ + public abstract class MongoRepositoryBase : IInitializable + { + private const string CollectionFormat = "{0}Set"; + + protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + protected static readonly SortDefinitionBuilder Sort = Builders.Sort; + protected static readonly UpdateDefinitionBuilder Update = Builders.Update; + protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; + protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; + protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; + protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; + + private readonly IMongoDatabase mongoDatabase; + private Lazy> mongoCollection; + + protected IMongoCollection Collection + { + get { return mongoCollection.Value; } + } + + protected IMongoDatabase Database + { + get { return mongoDatabase; } + } + + static MongoRepositoryBase() + { + RefTokenSerializer.Register(); + + InstantSerializer.Register(); + } + + protected MongoRepositoryBase(IMongoDatabase database) + { + Guard.NotNull(database); + + mongoDatabase = database; + mongoCollection = CreateCollection(); + } + + protected virtual MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings(); + } + + protected virtual string CollectionName() + { + return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name); + } + + private Lazy> CreateCollection() + { + return new Lazy>(() => + mongoDatabase.GetCollection( + CollectionName(), + CollectionSettings() ?? new MongoCollectionSettings())); + } + + protected virtual Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return TaskHelper.Done; + } + + public virtual async Task ClearAsync() + { + await Database.DropCollectionAsync(CollectionName()); + + await SetupCollectionAsync(Collection); + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + try + { + await SetupCollectionAsync(Collection, ct); + } + catch (Exception ex) + { + throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs new file mode 100644 index 000000000..995af9d52 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Driver; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure.MongoDb.Queries +{ + public static class FilterBuilder + { + public static (FilterDefinition? Filter, bool Last) BuildFilter(this ClrQuery query, bool supportsSearch = true) + { + if (query.FullText != null) + { + if (!supportsSearch) + { + throw new ValidationException("Query $search clause not supported."); + } + + return (Builders.Filter.Text(query.FullText), false); + } + + if (query.Filter != null) + { + return (query.Filter.BuildFilter(), true); + } + + return (null, false); + } + + public static FilterDefinition BuildFilter(this FilterNode filterNode) + { + return FilterVisitor.Visit(filterNode); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs new file mode 100644 index 000000000..c3dc47147 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Linq; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Infrastructure.MongoDb.Queries +{ + public sealed class FilterVisitor : FilterNodeVisitor, ClrValue> + { + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + private static readonly FilterVisitor Instance = new FilterVisitor(); + + private FilterVisitor() + { + } + + public static FilterDefinition Visit(FilterNode node) + { + return node.Accept(Instance); + } + + public override FilterDefinition Visit(NegateFilter nodeIn) + { + return Filter.Not(nodeIn.Filter.Accept(this)); + } + + public override FilterDefinition Visit(LogicalFilter nodeIn) + { + if (nodeIn.Type == LogicalFilterType.And) + { + return Filter.And(nodeIn.Filters.Select(x => x.Accept(this))); + } + else + { + return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this))); + } + } + + public override FilterDefinition Visit(CompareFilter nodeIn) + { + var propertyName = nodeIn.Path.ToString(); + + var value = nodeIn.Value.Value; + + switch (nodeIn.Operator) + { + case CompareOperator.Empty: + return Filter.Or( + Filter.Exists(propertyName, false), + Filter.Eq(propertyName, default(T)!), + Filter.Eq(propertyName, string.Empty), + Filter.Eq(propertyName, new T[0])); + case CompareOperator.StartsWith: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); + case CompareOperator.Contains: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); + case CompareOperator.EndsWith: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); + case CompareOperator.Equals: + return Filter.Eq(propertyName, value); + case CompareOperator.GreaterThan: + return Filter.Gt(propertyName, value); + case CompareOperator.GreaterThanOrEqual: + return Filter.Gte(propertyName, value); + case CompareOperator.LessThan: + return Filter.Lt(propertyName, value); + case CompareOperator.LessThanOrEqual: + return Filter.Lte(propertyName, value); + case CompareOperator.NotEquals: + return Filter.Ne(propertyName, value); + case CompareOperator.In: + return Filter.In(propertyName, ((IList)value!).OfType()); + } + + throw new NotSupportedException(); + } + + private static BsonRegularExpression BuildRegex(CompareFilter node, Func formatter) + { + return new BsonRegularExpression(formatter(node.Value.Value!.ToString()!), "i"); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs new file mode 100644 index 000000000..0ec5b5db4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using MongoDB.Driver; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Infrastructure.MongoDb.Queries +{ + public static class SortBuilder + { + public static SortDefinition? BuildSort(this ClrQuery query) + { + if (query.Sort.Count > 0) + { + var sorts = new List>(); + + foreach (var sort in query.Sort) + { + sorts.Add(OrderBy(sort)); + } + + if (sorts.Count > 1) + { + return Builders.Sort.Combine(sorts); + } + else + { + return sorts[0]; + } + } + + return null; + } + + public static SortDefinition OrderBy(SortNode sort) + { + var propertyName = string.Join(".", sort.Path); + + if (sort.Order == SortOrder.Ascending) + { + return Builders.Sort.Ascending(propertyName); + } + else + { + return Builders.Sort.Descending(propertyName); + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj new file mode 100644 index 000000000..9a73cc684 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs new file mode 100644 index 000000000..647324ea7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.States +{ + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore where TKey : notnull + { + public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) + : base(database) + { + Guard.NotNull(jsonSerializer); + + BsonJsonConvention.Register(jsonSerializer); + } + + protected override string CollectionName() + { + var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); + + var name = attribute?.Name ?? typeof(T).Name; + + return $"States_{name}"; + } + + public async Task<(T Value, long Version)> ReadAsync(TKey key) + { + using (Profiler.TraceMethod>()) + { + var existing = + await Collection.Find(x => x.Id.Equals(key)) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.Doc, existing.Version); + } + + return (default, EtagVersion.NotFound); + } + } + + public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod>()) + { + await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); + } + } + + public async Task ReadAllAsync(Func callback, CancellationToken ct = default) + { + using (Profiler.TraceMethod>()) + { + await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(x.Doc, x.Version), ct); + } + } + + public async Task RemoveAsync(TKey key) + { + using (Profiler.TraceMethod>()) + { + await Collection.DeleteOneAsync(x => x.Id.Equals(key)); + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/States/MongoState.cs rename to backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs similarity index 100% rename from src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs rename to backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs diff --git a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs new file mode 100644 index 000000000..e71a14257 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// 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 System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class MongoUsageRepository : MongoRepositoryBase, IUsageRepository + { + private static readonly BulkWriteOptions Unordered = new BulkWriteOptions { IsOrdered = false }; + + public MongoUsageRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "UsagesV2"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index + .Ascending(x => x.Key) + .Ascending(x => x.Category) + .Ascending(x => x.Date)), + cancellationToken: ct); + } + + public async Task TrackUsagesAsync(UsageUpdate update) + { + Guard.NotNull(update); + + if (update.Counters.Count > 0) + { + var (filter, updateStatement) = CreateOperation(update); + + await Collection.UpdateOneAsync(filter, updateStatement, Upsert); + } + } + + public async Task TrackUsagesAsync(params UsageUpdate[] updates) + { + if (updates.Length == 1) + { + await TrackUsagesAsync(updates[0]); + } + else if (updates.Length > 0) + { + var writes = new List>(); + + foreach (var update in updates) + { + if (update.Counters.Count > 0) + { + var (filter, updateStatement) = CreateOperation(update); + + writes.Add(new UpdateOneModel(filter, updateStatement) { IsUpsert = true }); + } + } + + await Collection.BulkWriteAsync(writes, Unordered); + } + } + + private static (FilterDefinition, UpdateDefinition) CreateOperation(UsageUpdate usageUpdate) + { + var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; + + var update = Update + .SetOnInsert(x => x.Key, usageUpdate.Key) + .SetOnInsert(x => x.Date, usageUpdate.Date) + .SetOnInsert(x => x.Category, usageUpdate.Category); + + foreach (var counter in usageUpdate.Counters) + { + update = update.Inc($"Counters.{counter.Key}", counter.Value); + } + + var filter = Filter.Eq(x => x.Id, id); + + return (filter, update); + } + + public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); + + return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs new file mode 100644 index 000000000..4bbaaf222 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.CQRS.Events +{ + public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer + { + private readonly IJsonSerializer jsonSerializer; + private readonly string eventPublisherName; + private readonly string exchange; + private readonly string eventsFilter; + private readonly ConnectionFactory connectionFactory; + private readonly Lazy connection; + private readonly Lazy channel; + + public string Name + { + get { return eventPublisherName; } + } + + public string EventsFilter + { + get { return eventsFilter; } + } + + public RabbitMqEventConsumer(IJsonSerializer jsonSerializer, string eventPublisherName, string uri, string exchange, string eventsFilter) + { + Guard.NotNullOrEmpty(uri); + Guard.NotNullOrEmpty(eventPublisherName); + Guard.NotNullOrEmpty(exchange); + Guard.NotNull(jsonSerializer); + + connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; + connection = new Lazy(connectionFactory.CreateConnection); + channel = new Lazy(connection.Value.CreateModel); + + this.exchange = exchange; + this.eventsFilter = eventsFilter; + this.eventPublisherName = eventPublisherName; + this.jsonSerializer = jsonSerializer; + } + + protected override void DisposeObject(bool disposing) + { + if (connection.IsValueCreated) + { + connection.Value.Close(); + connection.Value.Dispose(); + } + } + + public Task InitializeAsync(CancellationToken ct = default) + { + try + { + var currentConnection = connection.Value; + + if (!currentConnection.IsOpen) + { + throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}"); + } + + return TaskHelper.Done; + } + catch (Exception e) + { + throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}", e); + } + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public Task On(Envelope @event) + { + var jsonString = jsonSerializer.Serialize(@event); + var jsonBytes = Encoding.UTF8.GetBytes(jsonString); + + channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj new file mode 100644 index 000000000..37b1669f6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj @@ -0,0 +1,27 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + full + True + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs similarity index 100% rename from src/Squidex.Infrastructure.Redis/RedisPubSub.cs rename to backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs diff --git a/src/Squidex.Infrastructure.Redis/RedisSubscription.cs b/backend/src/Squidex.Infrastructure.Redis/RedisSubscription.cs similarity index 100% rename from src/Squidex.Infrastructure.Redis/RedisSubscription.cs rename to backend/src/Squidex.Infrastructure.Redis/RedisSubscription.cs diff --git a/backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj new file mode 100644 index 000000000..6edffe3ec --- /dev/null +++ b/backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj @@ -0,0 +1,26 @@ + + + netcoreapp3.0 + Squidex.Infrastructure + 7.3 + + + full + True + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs b/backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs new file mode 100644 index 000000000..1ab0d2964 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.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 System.Runtime.Serialization; + +namespace Squidex.Infrastructure.Assets +{ + [Serializable] + public class AssetAlreadyExistsException : Exception + { + public AssetAlreadyExistsException(string fileName) + : base(FormatMessage(fileName)) + { + } + + public AssetAlreadyExistsException(string fileName, Exception inner) + : base(FormatMessage(fileName), inner) + { + } + + protected AssetAlreadyExistsException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string FormatMessage(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + return $"An asset with name '{fileName}' already exists."; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs b/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs new file mode 100644 index 000000000..9f5a9aff3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetFile.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class AssetFile + { + private readonly Func openAction; + + public string FileName { get; } + + public string MimeType { get; } + + public long FileSize { get; } + + public AssetFile(string fileName, string mimeType, long fileSize, Func openAction) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNullOrEmpty(mimeType); + Guard.GreaterEquals(fileSize, 0); + + FileName = fileName; + FileSize = fileSize; + + MimeType = mimeType; + + this.openAction = openAction; + } + + public Stream OpenRead() + { + return openAction(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs b/backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs new file mode 100644 index 000000000..7883a56b9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetNotFoundException.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 System.Runtime.Serialization; + +namespace Squidex.Infrastructure.Assets +{ + [Serializable] + public class AssetNotFoundException : Exception + { + public AssetNotFoundException(string fileName) + : base(FormatMessage(fileName)) + { + } + + public AssetNotFoundException(string fileName, Exception inner) + : base(FormatMessage(fileName), inner) + { + } + + protected AssetNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string FormatMessage(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + return $"An asset with name '{fileName}' does not exist."; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs b/backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs new file mode 100644 index 000000000..00fae3d2d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public static class AssetStoreExtensions + { + public static string? GeneratePublicUrl(this IAssetStore store, Guid id, long version, string? suffix) + { + return store.GeneratePublicUrl(id.ToString(), version, suffix); + } + + public static string? GeneratePublicUrl(this IAssetStore store, string id, long version, string? suffix) + { + return store.GeneratePublicUrl(GetFileName(id, version, suffix)); + } + + public static Task CopyAsync(this IAssetStore store, string sourceFileName, Guid id, long version, string? suffix, CancellationToken ct = default) + { + return store.CopyAsync(sourceFileName, id.ToString(), version, suffix, ct); + } + + public static Task CopyAsync(this IAssetStore store, string sourceFileName, string id, long version, string? suffix, CancellationToken ct = default) + { + return store.CopyAsync(sourceFileName, GetFileName(id, version, suffix), ct); + } + + public static Task DownloadAsync(this IAssetStore store, Guid id, long version, string? suffix, Stream stream, CancellationToken ct = default) + { + return store.DownloadAsync(id.ToString(), version, suffix, stream, ct); + } + + public static Task DownloadAsync(this IAssetStore store, string id, long version, string? suffix, Stream stream, CancellationToken ct = default) + { + return store.DownloadAsync(GetFileName(id, version, suffix), stream, ct); + } + + public static Task UploadAsync(this IAssetStore store, Guid id, long version, string? suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + return store.UploadAsync(id.ToString(), version, suffix, stream, overwrite, ct); + } + + public static Task UploadAsync(this IAssetStore store, string id, long version, string? suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + return store.UploadAsync(GetFileName(id, version, suffix), stream, overwrite, ct); + } + + public static Task DeleteAsync(this IAssetStore store, Guid id, long version, string? suffix) + { + return store.DeleteAsync(id.ToString(), version, suffix); + } + + public static Task DeleteAsync(this IAssetStore store, string id, long version, string? suffix) + { + return store.DeleteAsync(GetFileName(id, version, suffix)); + } + + public static string GetFileName(string id, long version, string? suffix = null) + { + Guard.NotNullOrEmpty(id); + + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs new file mode 100644 index 000000000..a34f0c334 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentFTP; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class FTPAssetStore : IAssetStore, IInitializable + { + private readonly string path; + private readonly ISemanticLog log; + private readonly Func factory; + + public FTPAssetStore(Func factory, string path, ISemanticLog log) + { + Guard.NotNull(factory); + Guard.NotNullOrEmpty(path); + Guard.NotNull(log); + + this.factory = factory; + this.path = path; + + this.log = log; + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + using (var client = factory()) + { + await client.ConnectAsync(ct); + + if (!await client.DirectoryExistsAsync(path, ct)) + { + await client.CreateDirectoryAsync(path, ct); + } + } + + log.LogInformation(w => w + .WriteProperty("action", "FTPAssetStoreConfigured") + .WriteProperty("path", path)); + } + + public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + using (var client = GetFtpClient()) + { + var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + + using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) + { + await DownloadAsync(client, sourceFileName, stream, ct); + await UploadAsync(client, targetFileName, stream, false, ct); + } + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + using (var client = GetFtpClient()) + { + await DownloadAsync(client, fileName, stream, ct); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + using (var client = GetFtpClient()) + { + await UploadAsync(client, fileName, stream, overwrite, ct); + } + } + + private static async Task DownloadAsync(IFtpClient client, string fileName, Stream stream, CancellationToken ct) + { + try + { + await client.DownloadAsync(stream, fileName, token: ct); + } + catch (FtpException ex) when (IsNotFound(ex)) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct) + { + if (!overwrite && await client.FileExistsAsync(fileName, ct)) + { + throw new AssetAlreadyExistsException(fileName); + } + + await client.UploadAsync(stream, fileName, overwrite ? FtpExists.Overwrite : FtpExists.Skip, true, null, ct); + } + + public async Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + using (var client = GetFtpClient()) + { + try + { + await client.DeleteFileAsync(fileName); + } + catch (FtpException ex) + { + if (!IsNotFound(ex)) + { + throw; + } + } + } + } + + private IFtpClient GetFtpClient() + { + var client = factory(); + + client.Connect(); + client.SetWorkingDirectory(path); + + return client; + } + + private static bool IsNotFound(Exception exception) + { + if (exception is FtpCommandException command) + { + return command.CompletionCode == "550"; + } + + return exception.InnerException != null && IsNotFound(exception.InnerException); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs new file mode 100644 index 000000000..95d8ebdc6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class FolderAssetStore : IAssetStore, IInitializable + { + private const int BufferSize = 81920; + private readonly ISemanticLog log; + private readonly DirectoryInfo directory; + + public FolderAssetStore(string path, ISemanticLog log) + { + Guard.NotNullOrEmpty(path); + Guard.NotNull(log); + + this.log = log; + + directory = new DirectoryInfo(path); + } + + public Task InitializeAsync(CancellationToken ct = default) + { + try + { + if (!directory.Exists) + { + directory.Create(); + } + + log.LogInformation(w => w + .WriteProperty("action", "FolderAssetStoreConfigured") + .WriteProperty("path", directory.FullName)); + + return TaskHelper.Done; + } + catch (Exception ex) + { + throw new ConfigurationException($"Cannot access directory {directory.FullName}", ex); + } + } + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + var targetFile = GetFile(targetFileName); + var sourceFile = GetFile(sourceFileName); + + try + { + sourceFile.CopyTo(targetFile.FullName); + + return TaskHelper.Done; + } + catch (IOException) when (targetFile.Exists) + { + throw new AssetAlreadyExistsException(targetFileName); + } + catch (FileNotFoundException ex) + { + throw new AssetNotFoundException(sourceFileName, ex); + } + } + + public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNull(stream); + + var file = GetFile(fileName); + + try + { + using (var fileStream = file.OpenRead()) + { + await fileStream.CopyToAsync(stream, BufferSize, ct); + } + } + catch (FileNotFoundException ex) + { + throw new AssetNotFoundException(fileName, ex); + } + } + + public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNull(stream); + + var file = GetFile(fileName); + + try + { + using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write)) + { + await stream.CopyToAsync(fileStream, BufferSize, ct); + } + } + catch (IOException) when (file.Exists) + { + throw new AssetAlreadyExistsException(file.Name); + } + } + + public Task DeleteAsync(string fileName) + { + var file = GetFile(fileName); + + file.Delete(); + + return TaskHelper.Done; + } + + private FileInfo GetFile(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + return new FileInfo(GetPath(fileName)); + } + + private string GetPath(string name) + { + return Path.Combine(directory.FullName, name); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/HasherStream.cs b/backend/src/Squidex.Infrastructure/Assets/HasherStream.cs new file mode 100644 index 000000000..314466fa7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/HasherStream.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class HasherStream : Stream + { + private readonly Stream inner; + private readonly IncrementalHash hasher; + + public override bool CanRead + { + get { return inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return inner.Length; } + } + + public override long Position + { + get { return inner.Position; } + set { throw new NotSupportedException(); } + } + + public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName) + { + Guard.NotNull(inner); + + this.inner = inner; + + hasher = IncrementalHash.CreateHash(hashAlgorithmName); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var read = inner.Read(buffer, offset, count); + + if (read > 0) + { + hasher.AppendData(buffer, offset, read); + } + + return read; + } + + public byte[] GetHashAndReset() + { + return hasher.GetHashAndReset(); + } + + public string GetHashStringAndReset() + { + return Convert.ToBase64String(GetHashAndReset()); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs new file mode 100644 index 000000000..d00e89e65 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public interface IAssetStore + { + string? GeneratePublicUrl(string fileName); + + Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default); + + Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default); + + Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default); + + Task DeleteAsync(string fileName); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs new file mode 100644 index 000000000..7358dc7e4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public interface IAssetThumbnailGenerator + { + Task GetImageInfoAsync(Stream source); + + Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string? mode = null, int? quality = null); + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs new file mode 100644 index 000000000..707aac23b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Assets +{ + public sealed class ImageInfo + { + public int PixelWidth { get; } + + public int PixelHeight { get; } + + public ImageInfo(int pixelWidth, int pixelHeight) + { + Guard.GreaterThan(pixelWidth, 0); + Guard.GreaterThan(pixelHeight, 0); + + PixelWidth = pixelWidth; + PixelHeight = pixelHeight; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs new file mode 100644 index 000000000..57063172e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Transforms; +using SixLabors.Primitives; + +namespace Squidex.Infrastructure.Assets.ImageSharp +{ + public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator + { + public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string? mode = null, int? quality = null) + { + return Task.Run(() => + { + if (!width.HasValue && !height.HasValue && !quality.HasValue) + { + source.CopyTo(destination); + + return; + } + + using (var sourceImage = Image.Load(source, out var format)) + { + var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); + + if (quality.HasValue) + { + encoder = new JpegEncoder { Quality = quality.Value }; + } + + if (encoder == null) + { + throw new NotSupportedException(); + } + + if (width.HasValue || height.HasValue) + { + var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); + + if (!Enum.TryParse(mode, true, out var resizeMode)) + { + resizeMode = ResizeMode.Max; + } + + if (isCropUpsize) + { + resizeMode = ResizeMode.Crop; + } + + var resizeWidth = width ?? 0; + var resizeHeight = height ?? 0; + + if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) + { + resizeMode = ResizeMode.BoxPad; + } + + var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode }; + + sourceImage.Mutate(x => x.Resize(options)); + } + + sourceImage.Save(destination, encoder); + } + }); + } + + public Task GetImageInfoAsync(Stream source) + { + return Task.Run(() => + { + try + { + var image = Image.Load(source); + + return new ImageInfo(image.Width, image.Height); + } + catch + { + return null; + } + }); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs new file mode 100644 index 000000000..5e6feec8f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public class MemoryAssetStore : IAssetStore + { + private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); + private readonly AsyncLock readerLock = new AsyncLock(); + private readonly AsyncLock writerLock = new AsyncLock(); + + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName); + Guard.NotNullOrEmpty(targetFileName); + + if (!streams.TryGetValue(sourceFileName, out var sourceStream)) + { + throw new AssetNotFoundException(sourceFileName); + } + + using (await readerLock.LockAsync()) + { + await UploadAsync(targetFileName, sourceStream, false, ct); + } + } + + public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + if (!streams.TryGetValue(fileName, out var sourceStream)) + { + throw new AssetNotFoundException(fileName); + } + + using (await readerLock.LockAsync()) + { + try + { + await sourceStream.CopyToAsync(stream, 81920, ct); + } + finally + { + sourceStream.Position = 0; + } + } + } + + public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(fileName); + Guard.NotNull(stream); + + var memoryStream = new MemoryStream(); + + async Task CopyAsync() + { + using (await writerLock.LockAsync()) + { + try + { + await stream.CopyToAsync(memoryStream, 81920, ct); + } + finally + { + memoryStream.Position = 0; + } + } + } + + if (overwrite) + { + await CopyAsync(); + + streams[fileName] = memoryStream; + } + else if (streams.TryAdd(fileName, memoryStream)) + { + await CopyAsync(); + } + else + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public virtual Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName); + + streams.TryRemove(fileName, out _); + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs new file mode 100644 index 000000000..7f72920a8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class NoopAssetStore : IAssetStore + { + public string? GeneratePublicUrl(string fileName) + { + return null; + } + + public Task CopyAsync(string sourceFileName, string fileName, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DeleteAsync(string fileName) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs b/backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs new file mode 100644 index 000000000..9a7611c10 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading; +using Squidex.Infrastructure.Tasks; + +#pragma warning disable CS8601 // Possible null reference assignment. + +namespace Squidex.Infrastructure.Caching +{ + public sealed class AsyncLocalCache : ILocalCache + { + private static readonly AsyncLocal> LocalCache = new AsyncLocal>(); + private static readonly AsyncLocalCleaner> Cleaner; + + static AsyncLocalCache() + { + Cleaner = new AsyncLocalCleaner>(LocalCache); + } + + public IDisposable StartContext() + { + LocalCache.Value = new ConcurrentDictionary(); + + return Cleaner; + } + + public void Add(object key, object? value) + { + var cacheKey = GetCacheKey(key); + + var cache = LocalCache.Value; + + if (cache != null) + { + cache[cacheKey] = value; + } + } + + public void Remove(object key) + { + var cacheKey = GetCacheKey(key); + + var cache = LocalCache.Value; + + if (cache != null) + { + cache.TryRemove(cacheKey, out _); + } + } + + public bool TryGetValue(object key, out object? value) + { + var cacheKey = GetCacheKey(key); + + var cache = LocalCache.Value; + + if (cache != null) + { + return cache.TryGetValue(cacheKey, out value); + } + + value = null; + + return false; + } + + private static string GetCacheKey(object key) + { + return $"CACHE_{key}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs b/backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs new file mode 100644 index 000000000..b1248a6a2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Caching.Memory; + +namespace Squidex.Infrastructure.Caching +{ + public abstract class CachingProviderBase + { + private readonly IMemoryCache cache; + + protected IMemoryCache Cache + { + get { return cache; } + } + + protected CachingProviderBase(IMemoryCache cache) + { + Guard.NotNull(cache); + + this.cache = cache; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs b/backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs new file mode 100644 index 000000000..8be615ca3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Caching +{ + public interface ILocalCache + { + IDisposable StartContext(); + + void Add(object key, object? value); + + void Remove(object key); + + bool TryGetValue(object key, out object? value); + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/LRUCache.cs b/backend/src/Squidex.Infrastructure/Caching/LRUCache.cs new file mode 100644 index 000000000..29f4d8845 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/LRUCache.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class LRUCache where TKey : notnull + { + private readonly Dictionary>> cacheMap = new Dictionary>>(); + private readonly LinkedList> cacheHistory = new LinkedList>(); + private readonly int capacity; + private readonly Action itemEvicted; + + public LRUCache(int capacity, Action? itemEvicted = null) + { + Guard.GreaterThan(capacity, 0); + + this.capacity = capacity; + + this.itemEvicted = itemEvicted ?? ((key, value) => { }); + } + + public bool Set(TKey key, TValue value) + { + if (cacheMap.TryGetValue(key, out var node)) + { + node.Value.Value = value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + cacheMap[key] = node; + + return true; + } + + if (cacheMap.Count >= capacity) + { + RemoveFirst(); + } + + var cacheItem = new LRUCacheItem { Key = key, Value = value }; + + node = new LinkedListNode>(cacheItem); + + cacheMap.Add(key, node); + cacheHistory.AddLast(node); + + return false; + } + + public bool Remove(TKey key) + { + if (cacheMap.TryGetValue(key, out var node)) + { + cacheMap.Remove(key); + cacheHistory.Remove(node); + + return true; + } + + return false; + } + + public bool TryGetValue(TKey key, out object? value) + { + value = null; + + if (cacheMap.TryGetValue(key, out var node)) + { + value = node.Value.Value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + return true; + } + + return false; + } + + public bool Contains(TKey key) + { + return cacheMap.ContainsKey(key); + } + + private void RemoveFirst() + { + var node = cacheHistory.First; + + if (node != null) + { + itemEvicted(node.Value.Key, node.Value.Value); + + cacheMap.Remove(node.Value.Key); + cacheHistory.RemoveFirst(); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs b/backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs new file mode 100644 index 000000000..3a6b20ef4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +#pragma warning disable SA1401 // Fields must be private +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + +namespace Squidex.Infrastructure.Caching +{ + internal class LRUCacheItem + { + public TKey Key; + + public TValue Value; + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs b/backend/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs rename to backend/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs diff --git a/src/Squidex.Infrastructure/Cloneable.cs b/backend/src/Squidex.Infrastructure/Cloneable.cs similarity index 100% rename from src/Squidex.Infrastructure/Cloneable.cs rename to backend/src/Squidex.Infrastructure/Cloneable.cs diff --git a/src/Squidex.Infrastructure/Cloneable{T}.cs b/backend/src/Squidex.Infrastructure/Cloneable{T}.cs similarity index 100% rename from src/Squidex.Infrastructure/Cloneable{T}.cs rename to backend/src/Squidex.Infrastructure/Cloneable{T}.cs diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs new file mode 100644 index 000000000..379a65969 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -0,0 +1,234 @@ +// ========================================================================== +// 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; + +namespace Squidex.Infrastructure +{ + public static class CollectionExtensions + { + public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class + { + return ResultList.Create(input.Total, SortList(input, idProvider, ids)); + } + + public static IEnumerable SortList(this IEnumerable input, Func idProvider, IReadOnlyList ids) where T : class + { + return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).Where(x => x != null); + } + + public static void AddRange(this ICollection target, IEnumerable source) + { + foreach (var value in source) + { + target.Add(value); + } + } + + public static IEnumerable Shuffle(this IEnumerable enumerable) + { + var random = new Random(); + + return enumerable.OrderBy(x => random.Next()).ToList(); + } + + public static IEnumerable OrEmpty(this IEnumerable? source) + { + return source ?? Enumerable.Empty(); + } + + public static IEnumerable Concat(this IEnumerable source, T value) + { + return source.Concat(Enumerable.Repeat(value, 1)); + } + + public static TResult[] Map(this T[] value, Func convert) + { + var result = new TResult[value.Length]; + + for (var i = 0; i < value.Length; i++) + { + result[i] = convert(value[i]); + } + + return result; + } + + public static int SequentialHashCode(this IEnumerable collection) + { + return collection.SequentialHashCode(EqualityComparer.Default); + } + + public static int SequentialHashCode(this IEnumerable collection, IEqualityComparer comparer) + { + var hashCode = 17; + + foreach (var item in collection) + { + if (!Equals(item, null)) + { + hashCode = (hashCode * 23) + comparer.GetHashCode(item); + } + } + + return hashCode; + } + + public static int OrderedHashCode(this IEnumerable collection) where T : notnull + { + return collection.OrderedHashCode(EqualityComparer.Default); + } + + public static int OrderedHashCode(this IEnumerable collection, IEqualityComparer comparer) where T : notnull + { + Guard.NotNull(comparer); + + var hashCodes = collection.Where(x => !Equals(x, null)).Select(x => x.GetHashCode()).OrderBy(x => x).ToArray(); + + var hashCode = 17; + + foreach (var code in hashCodes) + { + hashCode = (hashCode * 23) + code; + } + + return hashCode; + } + + public static int DictionaryHashCode(this IDictionary dictionary) where TKey : notnull + { + return DictionaryHashCode(dictionary, EqualityComparer.Default, EqualityComparer.Default); + } + + public static int DictionaryHashCode(this IDictionary dictionary, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + { + var hashCode = 17; + + foreach (var kvp in dictionary.OrderBy(x => x.Key)) + { + hashCode = (hashCode * 23) + keyComparer.GetHashCode(kvp.Key); + + if (!Equals(kvp.Value, null)) + { + hashCode = (hashCode * 23) + valueComparer.GetHashCode(kvp.Value); + } + } + + return hashCode; + } + + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other) where TKey : notnull + { + return EqualsDictionary(dictionary, other, EqualityComparer.Default, EqualityComparer.Default); + } + + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + { + var comparer = new KeyValuePairComparer(keyComparer, valueComparer); + + return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any(); + } + + public static TValue GetOrDefault(this IReadOnlyDictionary dictionary, TKey key) where TKey : notnull + { + return dictionary.GetOrCreate(key, _ => default!); + } + + public static TValue GetOrAddDefault(this IDictionary dictionary, TKey key) where TKey : notnull + { + return dictionary.GetOrAdd(key, _ => default!); + } + + public static TValue GetOrNew(this IReadOnlyDictionary dictionary, TKey key) where TKey : notnull where TValue : class, new() + { + return dictionary.GetOrCreate(key, _ => new TValue()); + } + + public static TValue GetOrAddNew(this IDictionary dictionary, TKey key) where TKey : notnull where TValue : class, new() + { + return dictionary.GetOrAdd(key, _ => new TValue()); + } + + public static TValue GetOrCreate(this IReadOnlyDictionary dictionary, TKey key, Func creator) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = creator(key); + } + + return result; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TValue fallback) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = fallback; + + dictionary.Add(key, result); + } + + return result; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func creator) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = creator(key); + + dictionary.Add(key, result); + } + + return result; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TContext context, Func creator) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = creator(key, context); + + dictionary.Add(key, result); + } + + return result; + } + + public static void Foreach(this IEnumerable collection, Action action) + { + foreach (var item in collection) + { + action(item); + } + } + + public sealed class KeyValuePairComparer : IEqualityComparer> + { + private readonly IEqualityComparer keyComparer; + private readonly IEqualityComparer valueComparer; + + public KeyValuePairComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) + { + this.keyComparer = keyComparer; + this.valueComparer = valueComparer; + } + + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value); + } + + public int GetHashCode(KeyValuePair obj) + { + return keyComparer.GetHashCode(obj.Key) ^ valueComparer.GetHashCode(obj.Value); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs new file mode 100644 index 000000000..0ac20b261 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Collections +{ + public static class ArrayDictionary + { + public static ArrayDictionary ToArrayDictionary(this IEnumerable source, Func keyExtractor) where TKey : notnull + { + return new ArrayDictionary(source.Select(x => new KeyValuePair(keyExtractor(x), x)).ToArray()); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs new file mode 100644 index 000000000..86a81a006 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// 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.Infrastructure.Collections +{ + public class ArrayDictionary : IReadOnlyDictionary where TKey : notnull + { + private readonly IEqualityComparer keyComparer; + private readonly KeyValuePair[] items; + + public TValue this[TKey key] + { + get + { + if (!TryGetValue(key, out var value)) + { + throw new KeyNotFoundException(); + } + + return value; + } + } + + public IEnumerable Keys + { + get { return items.Select(x => x.Key); } + } + + public IEnumerable Values + { + get { return items.Select(x => x.Value); } + } + + public int Count + { + get { return items.Length; } + } + + public ArrayDictionary() + : this(EqualityComparer.Default, Array.Empty>()) + { + } + + public ArrayDictionary(KeyValuePair[] items) + : this(EqualityComparer.Default, items) + { + } + + public ArrayDictionary(IEqualityComparer keyComparer, KeyValuePair[] items) + { + Guard.NotNull(items, nameof(items)); + Guard.NotNull(keyComparer, nameof(keyComparer)); + + this.items = items; + + this.keyComparer = keyComparer; + } + + public KeyValuePair[] With(TKey key, TValue value) + { + var result = new List>(Math.Max(items.Length, 1)); + + var wasReplaced = false; + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + + if (wasReplaced || !keyComparer.Equals(item.Key, key)) + { + result.Add(item); + } + else + { + result.Add(new KeyValuePair(key, value)); + wasReplaced = true; + } + } + + if (!wasReplaced) + { + result.Add(new KeyValuePair(key, value)); + } + + return result.ToArray(); + } + + public KeyValuePair[] Without(TKey key) + { + var result = new List>(Math.Max(items.Length, 1)); + + var wasRemoved = false; + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + + if (wasRemoved || !keyComparer.Equals(item.Key, key)) + { + result.Add(item); + } + else + { + wasRemoved = true; + } + } + + return result.ToArray(); + } + + public bool ContainsKey(TKey key) + { + for (var i = 0; i < items.Length; i++) + { + if (keyComparer.Equals(items[i].Key, key)) + { + return true; + } + } + + return false; + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + for (var i = 0; i < items.Length; i++) + { + if (keyComparer.Equals(items[i].Key, key)) + { + value = items[i].Value; + return true; + } + } + + value = default!; + + return false; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerable(items).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + + private static IEnumerable GetEnumerable(IEnumerable array) + { + return array; + } + } +} diff --git a/src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs b/backend/src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs similarity index 100% rename from src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs rename to backend/src/Squidex.Infrastructure/Collections/ReadOnlyCollection.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs new file mode 100644 index 000000000..ccc82b13c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class CommandContext + { + private Tuple? result; + + public Guid ContextId { get; } = Guid.NewGuid(); + + public ICommand Command { get; } + + public ICommandBus CommandBus { get; } + + public object? PlainResult + { + get { return result?.Item1; } + } + + public bool IsCompleted + { + get { return result != null; } + } + + public CommandContext(ICommand command, ICommandBus commandBus) + { + Guard.NotNull(command); + Guard.NotNull(commandBus); + + Command = command; + CommandBus = commandBus; + } + + public CommandContext Complete(object? resultValue = null) + { + result = Tuple.Create(resultValue); + + return this; + } + + public T Result() + { + return (T)result?.Item1!; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/CommandExtensions.cs rename to backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs new file mode 100644 index 000000000..cbb7f4910 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class CustomCommandMiddlewareRunner : ICommandMiddleware + { + private readonly IEnumerable extensions; + + public CustomCommandMiddlewareRunner(IEnumerable extensions) + { + Guard.NotNull(extensions); + + this.extensions = extensions.Reverse().ToList(); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + foreach (var handler in extensions) + { + next = Join(handler, context, next); + } + + await next(); + } + + private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) + { + return () => handler.HandleAsync(context, next); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs new file mode 100644 index 000000000..ea63d15da --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.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.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class DomainObjectGrain : DomainObjectGrainBase where T : class, IDomainState, new() + { + private readonly IStore store; + private T snapshot = new T { Version = EtagVersion.Empty }; + private IPersistence? persistence; + + public override T Snapshot + { + get { return snapshot; } + } + + protected DomainObjectGrain(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(store); + + this.store = store; + } + + protected sealed override void ApplyEvent(Envelope @event) + { + var newVersion = Version + 1; + + snapshot = OnEvent(@event); + snapshot.Version = newVersion; + } + + protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) + { + snapshot = previousSnapshot; + } + + protected sealed override Task ReadAsync(Type type, Guid id) + { + persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), ApplyEvent); + + return persistence.ReadAsync(); + } + + private void ApplySnapshot(T state) + { + snapshot = state; + } + + protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) + { + if (events.Length > 0 && persistence != null) + { + await persistence.WriteEventsAsync(events); + await persistence.WriteSnapshotAsync(Snapshot); + } + } + + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs new file mode 100644 index 000000000..65358a154 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs @@ -0,0 +1,226 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() + { + private readonly List> uncomittedEvents = new List>(); + private readonly ISemanticLog log; + private Guid id; + + private enum Mode + { + Create, + Update, + Upsert + } + + public Guid Id + { + get { return id; } + } + + public long Version + { + get { return Snapshot.Version; } + } + + public abstract T Snapshot { get; } + + protected DomainObjectGrainBase(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + protected override async Task OnActivateAsync(Guid key) + { + var logContext = (key: key.ToString(), name: GetType().Name); + + using (log.MeasureInformation(logContext, (ctx, w) => w + .WriteProperty("action", "ActivateDomainObject") + .WriteProperty("domainObjectType", ctx.name) + .WriteProperty("domainObjectKey", ctx.key))) + { + id = key; + + await ReadAsync(GetType(), id); + } + } + + public void RaiseEvent(IEvent @event) + { + RaiseEvent(Envelope.Create(@event)); + } + + public virtual void RaiseEvent(Envelope @event) + { + Guard.NotNull(@event); + + @event.SetAggregateId(id); + + ApplyEvent(@event); + + uncomittedEvents.Add(@event); + } + + public IReadOnlyList> GetUncomittedEvents() + { + return uncomittedEvents; + } + + public void ClearUncommittedEvents() + { + uncomittedEvents.Clear(); + } + + protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, Mode.Create); + } + + protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync()!, Mode.Create); + } + + protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler.ToDefault(), Mode.Create); + } + + protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Create); + } + + protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, Mode.Update); + } + + protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync()!, Mode.Update); + } + + protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()!, Mode.Update); + } + + protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Update); + } + + protected Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, Mode.Upsert); + } + + protected Task UpsertReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync()!, Mode.Upsert); + } + + protected Task UpsertAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()!, Mode.Upsert); + } + + protected Task Upsert(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Upsert); + } + + private async Task InvokeAsync(TCommand command, Func> handler, Mode mode) where TCommand : class, IAggregateCommand + { + Guard.NotNull(command); + Guard.NotNull(handler); + + if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) + { + throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); + } + + if (mode == Mode.Update && Version < 0) + { + TryDeactivateOnIdle(); + + throw new DomainObjectNotFoundException(id.ToString(), GetType()); + } + + if (mode == Mode.Create && Version >= 0) + { + throw new DomainException("Object has already been created."); + } + + var previousSnapshot = Snapshot; + var previousVersion = Version; + try + { + var result = await handler(command); + + var events = uncomittedEvents.ToArray(); + + await WriteAsync(events, previousVersion); + + if (result == null) + { + if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0)) + { + result = new EntitySavedResult(Version); + } + else + { + result = EntityCreatedResult.Create(id, Version); + } + } + + return result; + } + catch + { + RestorePreviousSnapshot(previousSnapshot, previousVersion); + + throw; + } + finally + { + ClearUncommittedEvents(); + } + } + + protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); + + protected abstract void ApplyEvent(Envelope @event); + + protected abstract Task ReadAsync(Type type, Guid id); + + protected abstract Task WriteAsync(Envelope[] events, long previousVersion); + + public async Task> ExecuteAsync(J command) + { + var result = await ExecuteAsync(command.Value); + + return result; + } + + protected abstract Task ExecuteAsync(IAggregateCommand command); + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs new file mode 100644 index 000000000..5963bdec0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public static class DomainObjectGrainFormatter + { + public static string Format(IGrainCallContext context) + { + if (context.InterfaceMethod == null) + { + return "Unknown"; + } + + if (string.Equals(context.InterfaceMethod.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && + context.Arguments?.Length == 1 && + context.Arguments[0] != null) + { + var argumentFullName = context.Arguments[0].ToString(); + + if (argumentFullName != null) + { + var argumentParts = argumentFullName.Split('.'); + var argumentName = argumentParts[^1]; + + return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; + } + } + + return context.InterfaceMethod.Name; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs new file mode 100644 index 000000000..0d2ecd412 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using NodaTime; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware + { + private readonly IClock clock; + + public EnrichWithTimestampCommandMiddleware(IClock clock) + { + Guard.NotNull(clock); + + this.clock = clock; + } + + public Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is ITimestampCommand timestampCommand) + { + timestampCommand.Timestamp = clock.GetCurrentInstant(); + } + + return next(); + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs b/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs rename to backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult.cs diff --git a/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs b/backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs rename to backend/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs diff --git a/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs b/backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/EntitySavedResult.cs rename to backend/src/Squidex.Infrastructure/Commands/EntitySavedResult.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs new file mode 100644 index 000000000..0829900cd --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain + { + private readonly IGrainFactory grainFactory; + + public GrainCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public virtual async Task HandleAsync(CommandContext context, Func next) + { + await ExecuteCommandAsync(context); + + await next(); + } + + protected async Task ExecuteCommandAsync(CommandContext context) + { + if (context.Command is TCommand typedCommand) + { + var result = await ExecuteCommandAsync(typedCommand); + + context.Complete(result); + } + } + + private async Task ExecuteCommandAsync(TCommand typedCommand) + { + var grain = grainFactory.GetGrain(typedCommand.AggregateId); + + var result = await grain.ExecuteAsync(typedCommand); + + return result.Value; + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs b/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/IAggregateCommand.cs rename to backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs diff --git a/src/Squidex.Infrastructure/Commands/ICommand.cs b/backend/src/Squidex.Infrastructure/Commands/ICommand.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICommand.cs rename to backend/src/Squidex.Infrastructure/Commands/ICommand.cs diff --git a/src/Squidex.Infrastructure/Commands/ICommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICommandBus.cs rename to backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs diff --git a/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs rename to backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs diff --git a/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs rename to backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs new file mode 100644 index 000000000..ea10c037b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public interface IDomainObjectGrain : IGrainWithGuidKey + { + Task> ExecuteAsync(J command); + } +} diff --git a/src/Squidex.Infrastructure/Commands/IDomainState.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/IDomainState.cs rename to backend/src/Squidex.Infrastructure/Commands/IDomainState.cs diff --git a/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs b/backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ITimestampCommand.cs rename to backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs diff --git a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs new file mode 100644 index 000000000..987f81711 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class InMemoryCommandBus : ICommandBus + { + private readonly List middlewares; + + public InMemoryCommandBus(IEnumerable middlewares) + { + Guard.NotNull(middlewares); + + this.middlewares = middlewares.Reverse().ToList(); + } + + public async Task PublishAsync(ICommand command) + { + Guard.NotNull(command); + + var context = new CommandContext(command, this); + + var next = new Func(() => TaskHelper.Done); + + foreach (var handler in middlewares) + { + next = Join(handler, context, next); + } + + await next(); + + return context; + } + + private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) + { + return () => handler.HandleAsync(context, next); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs new file mode 100644 index 000000000..367cfdd4e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class LogCommandMiddleware : ICommandMiddleware + { + private readonly ISemanticLog log; + + public LogCommandMiddleware(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + var logContext = (id: context.ContextId.ToString(), command: context.Command.GetType().Name); + + try + { + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Started") + .WriteProperty("commandType", ctx.command)); + + using (log.MeasureInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Completed") + .WriteProperty("commandType", ctx.command))) + { + await next(); + } + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Succeeded") + .WriteProperty("commandType", ctx.command)); + } + catch (Exception ex) + { + log.LogError(ex, logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Failed") + .WriteProperty("commandType", ctx.command)); + + throw; + } + + if (!context.IsCompleted) + { + log.LogFatal(logContext, (ctx, w) => w + .WriteProperty("action", "HandleCommand.") + .WriteProperty("actionId", ctx.id) + .WriteProperty("status", "Unhandled") + .WriteProperty("commandType", ctx.command)); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs new file mode 100644 index 000000000..bbfb4cd91 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : class, IDomainState, new() + { + private readonly IStore store; + private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; + private IPersistence? persistence; + + public override T Snapshot + { + get { return snapshots.Last(); } + } + + protected LogSnapshotDomainObjectGrain(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(log); + + this.store = store; + } + + public T GetSnapshot(long version) + { + if (version == EtagVersion.Any || version == EtagVersion.Auto) + { + return Snapshot; + } + + if (version == EtagVersion.Empty) + { + return snapshots[0]; + } + + if (version >= 0 && version < snapshots.Count - 1) + { + return snapshots[(int)version + 1]; + } + + return default!; + } + + protected sealed override void ApplyEvent(Envelope @event) + { + var snapshot = OnEvent(@event); + + snapshot.Version = Version + 1; + snapshots.Add(snapshot); + } + + protected sealed override Task ReadAsync(Type type, Guid id) + { + persistence = store.WithEventSourcing(type, id, ApplyEvent); + + return persistence.ReadAsync(); + } + + protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) + { + if (events.Length > 0 && persistence != null) + { + var persistedSnapshots = store.GetSnapshotStore(); + + await persistence.WriteEventsAsync(events); + await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); + } + } + + protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) + { + while (snapshots.Count > previousVersion + 2) + { + snapshots.RemoveAt(snapshots.Count - 1); + } + } + + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs new file mode 100644 index 000000000..81d2b54ec --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class ReadonlyCommandMiddleware : ICommandMiddleware + { + private readonly ReadonlyOptions options; + + public ReadonlyCommandMiddleware(IOptions options) + { + Guard.NotNull(options); + + this.options = options.Value; + } + + public Task HandleAsync(CommandContext context, Func next) + { + if (options.IsReadonly) + { + throw new DomainException("Application is in readonly mode at the moment."); + } + + return next(); + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs rename to backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs diff --git a/src/Squidex.Infrastructure/Configuration/Alternatives.cs b/backend/src/Squidex.Infrastructure/Configuration/Alternatives.cs similarity index 100% rename from src/Squidex.Infrastructure/Configuration/Alternatives.cs rename to backend/src/Squidex.Infrastructure/Configuration/Alternatives.cs diff --git a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs b/backend/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs rename to backend/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs diff --git a/src/Squidex.Infrastructure/ConfigurationException.cs b/backend/src/Squidex.Infrastructure/ConfigurationException.cs similarity index 100% rename from src/Squidex.Infrastructure/ConfigurationException.cs rename to backend/src/Squidex.Infrastructure/ConfigurationException.cs diff --git a/backend/src/Squidex.Infrastructure/DelegateDisposable.cs b/backend/src/Squidex.Infrastructure/DelegateDisposable.cs new file mode 100644 index 000000000..f24bd2913 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DelegateDisposable.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public sealed class DelegateDisposable : IDisposable + { + private readonly Action action; + + public DelegateDisposable(Action action) + { + Guard.NotNull(action); + + this.action = action; + } + + public void Dispose() + { + action(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs b/backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs new file mode 100644 index 000000000..e7fc8dad6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Squidex.Infrastructure; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class DependencyInjectionExtensions + { + public delegate void Registrator(Type serviceType, Func implementationFactory); + + public sealed class InterfaceRegistrator where T : notnull + { + private readonly Registrator register; + private readonly Registrator registerOptional; + + public InterfaceRegistrator(Registrator register, Registrator registerOptional) + { + this.register = register; + this.registerOptional = registerOptional; + + var interfaces = typeof(T).GetInterfaces(); + + if (interfaces.Contains(typeof(IInitializable))) + { + register(typeof(IInitializable), c => c.GetRequiredService()); + } + + if (interfaces.Contains(typeof(IBackgroundProcess))) + { + register(typeof(IBackgroundProcess), c => c.GetRequiredService()); + } + } + + public InterfaceRegistrator AsSelf() + { + return this; + } + + public InterfaceRegistrator AsOptional() + { + if (typeof(TInterface) != typeof(T)) + { + registerOptional(typeof(TInterface), c => c.GetRequiredService()); + } + + return this; + } + + public InterfaceRegistrator As() + { + if (typeof(TInterface) != typeof(T)) + { + register(typeof(TInterface), c => c.GetRequiredService()); + } + + return this; + } + } + + public static InterfaceRegistrator AddTransientAs(this IServiceCollection services, Func factory) where T : class + { + services.AddTransient(typeof(T), factory); + + return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); + } + + public static InterfaceRegistrator AddTransientAs(this IServiceCollection services) where T : class + { + services.AddTransient(); + + return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); + } + + public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, Func factory) where T : class + { + services.AddSingleton(typeof(T), factory); + + return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); + } + + public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services) where T : class + { + services.AddSingleton(); + + return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs new file mode 100644 index 000000000..1dcb2d582 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GCHealthCheck : IHealthCheck + { + private readonly long threshold; + + public GCHealthCheck(IOptions options) + { + Guard.NotNull(options); + + threshold = 1024 * 1024 * options.Value.Threshold; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var allocated = GC.GetTotalMemory(false); + + var data = new Dictionary + { + { "Allocated", allocated.ToReadableSize() }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "Gen2Collections", GC.CollectionCount(2) } + }; + + var status = allocated < threshold ? HealthStatus.Healthy : HealthStatus.Unhealthy; + + var message = $"Application must consume less than {threshold.ToReadableSize()} memory."; + + return Task.FromResult(new HealthCheckResult(status, message, data: data)); + } + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs rename to backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs new file mode 100644 index 000000000..a2cf9e4f4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Orleans; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class OrleansHealthCheck : IHealthCheck + { + private readonly IManagementGrain managementGrain; + + public OrleansHealthCheck(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + managementGrain = grainFactory.GetGrain(0); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var activationCount = await managementGrain.GetTotalActivationCount(); + + var status = activationCount > 0 ? + HealthStatus.Healthy : + HealthStatus.Unhealthy; + + return new HealthCheckResult(status, "Orleans must have at least one activation."); + } + } +} diff --git a/src/Squidex.Infrastructure/DisposableObjectBase.cs b/backend/src/Squidex.Infrastructure/DisposableObjectBase.cs similarity index 100% rename from src/Squidex.Infrastructure/DisposableObjectBase.cs rename to backend/src/Squidex.Infrastructure/DisposableObjectBase.cs diff --git a/backend/src/Squidex.Infrastructure/DomainException.cs b/backend/src/Squidex.Infrastructure/DomainException.cs new file mode 100644 index 000000000..666f0f65c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DomainException.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure +{ + [Serializable] + public class DomainException : Exception + { + public DomainException(string message) + : base(message) + { + } + + public DomainException(string message, Exception? inner) + : base(message, inner) + { + } + + protected DomainException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Squidex.Infrastructure/DomainForbiddenException.cs b/backend/src/Squidex.Infrastructure/DomainForbiddenException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainForbiddenException.cs rename to backend/src/Squidex.Infrastructure/DomainForbiddenException.cs diff --git a/src/Squidex.Infrastructure/DomainObjectDeletedException.cs b/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainObjectDeletedException.cs rename to backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs diff --git a/backend/src/Squidex.Infrastructure/DomainObjectException.cs b/backend/src/Squidex.Infrastructure/DomainObjectException.cs new file mode 100644 index 000000000..4742bcc33 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DomainObjectException.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure +{ + [Serializable] + public class DomainObjectException : Exception + { + public string? TypeName { get; } + + public string Id { get; } + + protected DomainObjectException(string message, string id, Type type, Exception? inner = null) + : base(message, inner) + { + Id = id; + + TypeName = type?.Name; + } + + protected DomainObjectException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Id = info.GetString(nameof(Id))!; + + TypeName = info.GetString(nameof(TypeName))!; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Id), Id); + info.AddValue(nameof(TypeName), TypeName); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs b/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainObjectNotFoundException.cs rename to backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs diff --git a/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs similarity index 100% rename from src/Squidex.Infrastructure/DomainObjectVersionException.cs rename to backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs diff --git a/src/Squidex.Infrastructure/Email/IEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs similarity index 100% rename from src/Squidex.Infrastructure/Email/IEmailSender.cs rename to backend/src/Squidex.Infrastructure/Email/IEmailSender.cs diff --git a/src/Squidex.Infrastructure/Email/SmptOptions.cs b/backend/src/Squidex.Infrastructure/Email/SmptOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Email/SmptOptions.cs rename to backend/src/Squidex.Infrastructure/Email/SmptOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs new file mode 100644 index 000000000..347cc48ec --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Net; +using System.Net.Mail; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Email +{ + public sealed class SmtpEmailSender : IEmailSender + { + private readonly SmtpClient smtpClient; + private readonly string sender; + + public SmtpEmailSender(IOptions options) + { + Guard.NotNull(options); + + var config = options.Value; + + smtpClient = new SmtpClient(config.Server, config.Port) + { + Credentials = new NetworkCredential( + config.Username, + config.Password), + EnableSsl = config.EnableSsl + }; + + sender = config.Sender; + } + + public Task SendAsync(string recipient, string subject, string body) + { + return smtpClient.SendMailAsync(sender, recipient, subject, body); + } + } +} diff --git a/src/Squidex.Infrastructure/EtagVersion.cs b/backend/src/Squidex.Infrastructure/EtagVersion.cs similarity index 100% rename from src/Squidex.Infrastructure/EtagVersion.cs rename to backend/src/Squidex.Infrastructure/EtagVersion.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs b/backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs b/backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs new file mode 100644 index 000000000..d594b2590 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class CompoundEventConsumer : IEventConsumer + { + private readonly IEventConsumer[] inners; + + public string Name { get; } + + public string EventsFilter { get; } + + public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners) + : this(first?.Name!, first!, inners) + { + } + + public CompoundEventConsumer(IEventConsumer[] inners) + { + Guard.NotNull(inners); + Guard.NotEmpty(inners); + + this.inners = inners; + + Name = inners.First().Name; + + var innerFilters = + this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) + .Select(x => $"({x.EventsFilter})"); + + EventsFilter = string.Join("|", innerFilters); + } + + public CompoundEventConsumer(string name, IEventConsumer first, params IEventConsumer[] inners) + { + Guard.NotNull(first); + Guard.NotNull(inners); + Guard.NotNullOrEmpty(name); + + this.inners = new[] { first }.Union(inners).ToArray(); + + Name = name; + + var innerFilters = + this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) + .Select(x => $"({x.EventsFilter})"); + + EventsFilter = string.Join("|", innerFilters); + } + + public bool Handles(StoredEvent @event) + { + return inners.Any(x => x.Handles(@event)); + } + + public Task ClearAsync() + { + return Task.WhenAll(inners.Select(i => i.ClearAsync())); + } + + public async Task On(Envelope @event) + { + foreach (var inner in inners) + { + await inner.On(@event); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs new file mode 100644 index 000000000..2e93cac7a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class DefaultEventDataFormatter : IEventDataFormatter + { + private readonly IJsonSerializer serializer; + private readonly TypeNameRegistry typeNameRegistry; + + public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) + { + Guard.NotNull(typeNameRegistry); + Guard.NotNull(serializer); + + this.typeNameRegistry = typeNameRegistry; + + this.serializer = serializer; + } + + public Envelope Parse(EventData eventData, Func? stringConverter = null) + { + var payloadType = typeNameRegistry.GetType(eventData.Type); + var payloadObj = serializer.Deserialize(eventData.Payload, payloadType, stringConverter); + + if (payloadObj is IMigrated migratedEvent) + { + payloadObj = migratedEvent.Migrate(); + + if (ReferenceEquals(migratedEvent, payloadObj)) + { + Debug.WriteLine("Migration should return new event."); + } + } + + var envelope = new Envelope(payloadObj, eventData.Headers); + + return envelope; + } + + public EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true) + { + var eventPayload = envelope.Payload; + + if (migrate && eventPayload is IMigrated migratedEvent) + { + eventPayload = migratedEvent.Migrate(); + + if (ReferenceEquals(migratedEvent, eventPayload)) + { + Debug.WriteLine("Migration should return new event."); + } + } + + var payloadType = typeNameRegistry.GetName(eventPayload.GetType()); + var payloadJson = serializer.Serialize(envelope.Payload); + + envelope.SetCommitId(commitId); + + return new EventData(payloadType, envelope.Headers, payloadJson); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Envelope.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs new file mode 100644 index 000000000..4742e8bfb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public class Envelope where T : class, IEvent + { + private readonly EnvelopeHeaders headers; + private readonly T payload; + + public EnvelopeHeaders Headers + { + get { return headers; } + } + + public T Payload + { + get { return payload; } + } + + public Envelope(T payload, EnvelopeHeaders? headers = null) + { + Guard.NotNull(payload); + + this.payload = payload; + + this.headers = headers ?? new EnvelopeHeaders(); + } + + public Envelope To() where TOther : class, IEvent + { + return new Envelope((payload as TOther)!, headers.Clone()); + } + + public static implicit operator Envelope(Envelope source) + { + return source == null ? source! : new Envelope(source.payload, source.headers); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs new file mode 100644 index 000000000..47198a953 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class EventData + { + public EnvelopeHeaders Headers { get; } + + public string Payload { get; } + + public string Type { get; set; } + + public EventData(string type, EnvelopeHeaders headers, string payload) + { + Guard.NotNull(type); + Guard.NotNull(headers); + Guard.NotNull(payload); + + Headers = headers; + + Payload = payload; + + Type = type; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs new file mode 100644 index 000000000..d997e6a84 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -0,0 +1,305 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerGrain : GrainOfString, IEventConsumerGrain + { + private readonly EventConsumerFactory eventConsumerFactory; + private readonly IGrainState state; + private readonly IEventDataFormatter eventDataFormatter; + private readonly IEventStore eventStore; + private readonly ISemanticLog log; + private TaskScheduler? scheduler; + private IEventSubscription? currentSubscription; + private IEventConsumer? eventConsumer; + + public EventConsumerGrain( + EventConsumerFactory eventConsumerFactory, + IGrainState state, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISemanticLog log) + { + Guard.NotNull(eventStore); + Guard.NotNull(eventDataFormatter); + Guard.NotNull(eventConsumerFactory); + Guard.NotNull(state); + Guard.NotNull(log); + + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.eventConsumerFactory = eventConsumerFactory; + this.state = state; + + this.log = log; + } + + protected override Task OnActivateAsync(string key) + { + scheduler = TaskScheduler.Current; + + eventConsumer = eventConsumerFactory(key); + + return TaskHelper.Done; + } + + public Task> GetStateAsync() + { + return Task.FromResult(CreateInfo()); + } + + private Immutable CreateInfo() + { + return state.Value.ToInfo(eventConsumer!.Name).AsImmutable(); + } + + public Task OnEventAsync(Immutable subscription, Immutable storedEvent) + { + if (subscription.Value != currentSubscription) + { + return TaskHelper.Done; + } + + return DoAndUpdateStateAsync(async () => + { + if (eventConsumer!.Handles(storedEvent.Value)) + { + var @event = ParseKnownEvent(storedEvent.Value); + + if (@event != null) + { + await DispatchConsumerAsync(@event); + } + } + + state.Value = state.Value.Handled(storedEvent.Value.EventPosition); + }); + } + + public Task OnErrorAsync(Immutable subscription, Immutable exception) + { + if (subscription.Value != currentSubscription) + { + return TaskHelper.Done; + } + + return DoAndUpdateStateAsync(() => + { + Unsubscribe(); + + state.Value = state.Value.Failed(exception.Value); + }); + } + + public Task ActivateAsync() + { + if (!state.Value.IsStopped) + { + Subscribe(state.Value.Position); + } + + return TaskHelper.Done; + } + + public async Task> StartAsync() + { + if (!state.Value.IsStopped) + { + return CreateInfo(); + } + + await DoAndUpdateStateAsync(() => + { + Subscribe(state.Value.Position); + + state.Value = state.Value.Started(); + }); + + return CreateInfo(); + } + + public async Task> StopAsync() + { + if (state.Value.IsStopped) + { + return CreateInfo(); + } + + await DoAndUpdateStateAsync(() => + { + Unsubscribe(); + + state.Value = state.Value.Stopped(); + }); + + return CreateInfo(); + } + + public async Task> ResetAsync() + { + await DoAndUpdateStateAsync(async () => + { + Unsubscribe(); + + await ClearAsync(); + + Subscribe(null); + + state.Value = state.Value.Reset(); + }); + + return CreateInfo(); + } + + private Task DoAndUpdateStateAsync(Action action, [CallerMemberName] string? caller = null) + { + return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }, caller); + } + + private async Task DoAndUpdateStateAsync(Func action, [CallerMemberName] string? caller = null) + { + try + { + await action(); + } + catch (Exception ex) + { + try + { + Unsubscribe(); + } + catch (Exception unsubscribeException) + { + ex = new AggregateException(ex, unsubscribeException); + } + + log.LogFatal(ex, w => w + .WriteProperty("action", caller) + .WriteProperty("status", "Failed") + .WriteProperty("eventConsumer", eventConsumer!.Name)); + + state.Value = state.Value.Failed(ex); + } + + await state.WriteAsync(); + } + + private async Task ClearAsync() + { + var logContext = (actionId: Guid.NewGuid().ToString(), consumer: eventConsumer.Name); + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "EventConsumerReset") + .WriteProperty("actionId", ctx.actionId) + .WriteProperty("status", "Started") + .WriteProperty("eventConsumer", ctx.consumer)); + + using (log.MeasureTrace(logContext, (ctx, w) => w + .WriteProperty("action", "EventConsumerReset") + .WriteProperty("actionId", ctx.actionId) + .WriteProperty("status", "Completed") + .WriteProperty("eventConsumer", ctx.consumer))) + { + await eventConsumer.ClearAsync(); + } + } + + private async Task DispatchConsumerAsync(Envelope @event) + { + var eventId = @event.Headers.EventId().ToString(); + var eventType = @event.Payload.GetType().Name; + + var logContext = (eventId, eventType, consumer: eventConsumer.Name); + + log.LogInformation(logContext, (ctx, w) => w + .WriteProperty("action", "HandleEvent") + .WriteProperty("actionId", ctx.eventId) + .WriteProperty("status", "Started") + .WriteProperty("eventId", ctx.eventId) + .WriteProperty("eventType", ctx.eventType) + .WriteProperty("eventConsumer", ctx.consumer)); + + using (log.MeasureTrace(logContext, (ctx, w) => w + .WriteProperty("action", "HandleEvent") + .WriteProperty("actionId", ctx.eventId) + .WriteProperty("status", "Completed") + .WriteProperty("eventId", ctx.eventId) + .WriteProperty("eventType", ctx.eventType) + .WriteProperty("eventConsumer", ctx.consumer))) + { + await eventConsumer.On(@event); + } + } + + private void Unsubscribe() + { + if (currentSubscription != null) + { + currentSubscription.StopAsync().Forget(); + currentSubscription = null; + } + } + + private void Subscribe(string? position) + { + if (currentSubscription == null) + { + currentSubscription?.StopAsync().Forget(); + currentSubscription = CreateSubscription(eventConsumer!.EventsFilter, position); + } + else + { + currentSubscription.WakeUp(); + } + } + + private Envelope? ParseKnownEvent(StoredEvent message) + { + try + { + var @event = eventDataFormatter.Parse(message.Data); + + @event.SetEventPosition(message.EventPosition); + @event.SetEventStreamNumber(message.EventStreamNumber); + + return @event; + } + catch (TypeNameNotFoundException) + { + log.LogDebug(w => w.WriteProperty("oldEventFound", message.Data.Type)); + + return null; + } + } + + protected virtual IEventConsumerGrain GetSelf() + { + return this.AsReference(); + } + + protected virtual IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string? position) + { + return new RetrySubscription(store, subscriber, streamFilter, position); + } + + private IEventSubscription CreateSubscription(string streamFilter, string? position) + { + return CreateSubscription(eventStore, new WrapperSubscription(GetSelf(), scheduler!), streamFilter, position); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs new file mode 100644 index 000000000..329dee5da --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// 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 System.Text.RegularExpressions; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Orleans.Core; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerManagerGrain : Grain, IEventConsumerManagerGrain, IRemindable + { + private readonly IEnumerable eventConsumers; + + public EventConsumerManagerGrain(IEnumerable eventConsumers) + : this(eventConsumers, null, null) + { + } + + protected EventConsumerManagerGrain( + IEnumerable eventConsumers, + IGrainIdentity? identity, + IGrainRuntime? runtime) + : base(identity, runtime) + { + Guard.NotNull(eventConsumers); + + this.eventConsumers = eventConsumers; + } + + public override Task OnActivateAsync() + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); + } + + public Task ActivateAsync(string? streamName) + { + var tasks = + eventConsumers + .Where(c => streamName == null || Regex.IsMatch(streamName, c.EventsFilter)) + .Select(c => GrainFactory.GetGrain(c.Name)) + .Select(c => c.ActivateAsync()); + + return Task.WhenAll(tasks); + } + + public async Task>> GetConsumersAsync() + { + var tasks = + eventConsumers + .Select(c => GrainFactory.GetGrain(c.Name)) + .Select(c => c.GetStateAsync()); + + var consumerInfos = await Task.WhenAll(tasks); + + return new Immutable>(consumerInfos.Select(r => r.Value).ToList()); + } + + public Task StartAllAsync() + { + return Task.WhenAll( + eventConsumers + .Select(c => StartAsync(c.Name))); + } + + public Task StopAllAsync() + { + return Task.WhenAll( + eventConsumers + .Select(c => StopAsync(c.Name))); + } + + public Task> ResetAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.ResetAsync(); + } + + public Task> StartAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.StartAsync(); + } + + public Task> StopAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.StopAsync(); + } + + public Task ActivateAsync() + { + return ActivateAsync(null); + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return ActivateAsync(null); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs new file mode 100644 index 000000000..8a68081fc --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public sealed class EventConsumerState + { + public bool IsStopped { get; set; } + + public string? Error { get; set; } + + public string? Position { get; set; } + + public EventConsumerState Reset() + { + return new EventConsumerState(); + } + + public EventConsumerState Handled(string position) + { + return new EventConsumerState { Position = position }; + } + + public EventConsumerState Failed(Exception ex) + { + return new EventConsumerState { Position = Position, IsStopped = true, Error = ex?.ToString() }; + } + + public EventConsumerState Stopped() + { + return new EventConsumerState { Position = Position, IsStopped = true }; + } + + public EventConsumerState Started() + { + return new EventConsumerState { Position = Position, IsStopped = false }; + } + + public EventConsumerInfo ToInfo(string name) + { + return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs new file mode 100644 index 000000000..898464730 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public sealed class OrleansEventNotifier : IEventNotifier + { + private readonly Lazy eventConsumerManagerGrain; + + public OrleansEventNotifier(IGrainFactory factory) + { + Guard.NotNull(factory); + + eventConsumerManagerGrain = new Lazy(() => + { + return factory.GetGrain(SingleGrain.Id); + }); + } + + public void NotifyEventsStored(string streamName) + { + eventConsumerManagerGrain.Value.ActivateAsync(streamName); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEvent.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs new file mode 100644 index 000000000..38332ea9a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.EventSourcing +{ + public interface IEventDataFormatter + { + Envelope Parse(EventData eventData, Func? stringConverter = null); + + EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true); + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs new file mode 100644 index 000000000..881eb708c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public interface IEventStore + { + Task CreateIndexAsync(string property); + + Task> QueryAsync(string streamName, long streamPosition = 0); + + Task QueryAsync(Func callback, string? streamFilter = null, string? position = null, CancellationToken ct = default); + + Task QueryAsync(Func callback, string property, object value, string? position = null, CancellationToken ct = default); + + Task AppendAsync(Guid commitId, string streamName, ICollection events); + + Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); + + Task DeleteStreamAsync(string streamName); + + IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null); + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs diff --git a/src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/NoopEvent.cs diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs new file mode 100644 index 000000000..9323bdd86 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class PollingSubscription : IEventSubscription + { + private readonly CompletionTimer timer; + + public PollingSubscription( + IEventStore eventStore, + IEventSubscriber eventSubscriber, + string? streamFilter, + string? position) + { + Guard.NotNull(eventStore); + Guard.NotNull(eventSubscriber); + + timer = new CompletionTimer(5000, async ct => + { + try + { + await eventStore.QueryAsync(async storedEvent => + { + await eventSubscriber.OnEventAsync(this, storedEvent); + + position = storedEvent.EventPosition; + }, streamFilter, position, ct); + } + catch (Exception ex) + { + if (!ex.Is()) + { + await eventSubscriber.OnErrorAsync(this, ex); + } + } + }); + } + + public void WakeUp() + { + timer.SkipCurrentDelay(); + } + + public Task StopAsync() + { + return timer.StopAsync(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs new file mode 100644 index 000000000..229e62860 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +#pragma warning disable RECS0002 // Convert anonymous method to method group + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class RetrySubscription : IEventSubscription, IEventSubscriber + { + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(10); + private readonly CancellationTokenSource timerCts = new CancellationTokenSource(); + private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); + private readonly IEventStore eventStore; + private readonly IEventSubscriber eventSubscriber; + private readonly string? streamFilter; + private IEventSubscription? currentSubscription; + private string? position; + + public int ReconnectWaitMs { get; set; } = 5000; + + public RetrySubscription(IEventStore eventStore, IEventSubscriber eventSubscriber, string? streamFilter, string? position) + { + Guard.NotNull(eventStore); + Guard.NotNull(eventSubscriber); + Guard.NotNull(streamFilter); + + this.position = position; + + this.eventStore = eventStore; + this.eventSubscriber = eventSubscriber; + + this.streamFilter = streamFilter; + + Subscribe(); + } + + private void Subscribe() + { + if (currentSubscription == null) + { + currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); + } + } + + private void Unsubscribe() + { + currentSubscription?.StopAsync().Forget(); + currentSubscription = null; + } + + public void WakeUp() + { + currentSubscription?.WakeUp(); + } + + private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + if (subscription == currentSubscription) + { + await eventSubscriber.OnEventAsync(this, storedEvent); + + position = storedEvent.EventPosition; + } + } + + private async Task HandleErrorAsync(IEventSubscription subscription, Exception exception) + { + if (subscription == currentSubscription) + { + Unsubscribe(); + + if (retryWindow.CanRetryAfterFailure()) + { + RetryAsync().Forget(); + } + else + { + await eventSubscriber.OnErrorAsync(this, exception); + } + } + } + + private async Task RetryAsync() + { + await Task.Delay(ReconnectWaitMs, timerCts.Token); + + await dispatcher.DispatchAsync(Subscribe); + } + + Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent)); + } + + Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); + } + + public async Task StopAsync() + { + await dispatcher.DispatchAsync(Unsubscribe); + await dispatcher.StopAndWaitAsync(); + + timerCts.Cancel(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs new file mode 100644 index 000000000..e6418d7f3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed class StoredEvent + { + public string StreamName { get; } + + public string EventPosition { get; } + + public long EventStreamNumber { get; } + + public EventData Data { get; } + + public StoredEvent(string streamName, string eventPosition, long eventStreamNumber, EventData data) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNullOrEmpty(eventPosition); + Guard.NotNull(data); + + Data = data; + + EventPosition = eventPosition; + EventStreamNumber = eventStreamNumber; + + StreamName = streamName; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs new file mode 100644 index 000000000..9a343d050 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class StreamFilter + { + public static bool IsAll([NotNullWhen(false)] string? filter) + { + return string.IsNullOrWhiteSpace(filter) + || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs b/backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs similarity index 100% rename from src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs diff --git a/src/Squidex.Infrastructure/ExceptionHelper.cs b/backend/src/Squidex.Infrastructure/ExceptionHelper.cs similarity index 100% rename from src/Squidex.Infrastructure/ExceptionHelper.cs rename to backend/src/Squidex.Infrastructure/ExceptionHelper.cs diff --git a/src/Squidex.Infrastructure/FileExtensions.cs b/backend/src/Squidex.Infrastructure/FileExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/FileExtensions.cs rename to backend/src/Squidex.Infrastructure/FileExtensions.cs diff --git a/src/Squidex.Infrastructure/GravatarHelper.cs b/backend/src/Squidex.Infrastructure/GravatarHelper.cs similarity index 100% rename from src/Squidex.Infrastructure/GravatarHelper.cs rename to backend/src/Squidex.Infrastructure/GravatarHelper.cs diff --git a/backend/src/Squidex.Infrastructure/Guard.cs b/backend/src/Squidex.Infrastructure/Guard.cs new file mode 100644 index 000000000..f98164c84 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Guard.cs @@ -0,0 +1,220 @@ +// ========================================================================== +// 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; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure +{ + public static class Guard + { + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidNumber(float target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) + { + throw new ArgumentException("Value must be a valid number.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidNumber(double target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) + { + throw new ArgumentException("Value must be a valid number.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidSlug(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); + + if (!target!.IsSlug()) + { + throw new ArgumentException("Target is not a valid slug.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidPropertyName(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); + + if (!target!.IsPropertyName()) + { + throw new ArgumentException("Target is not a valid property name.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void HasType(object? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target != null && target.GetType() != typeof(T)) + { + throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void HasType(object? target, [AllowNull] Type expectedType, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target != null && expectedType != null && target.GetType() != expectedType) + { + throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Between(TValue target, TValue lower, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (!target.IsBetween(lower, upper)) + { + throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Enum(TEnum target, [CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct + { + if (!target.IsEnumValue()) + { + throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GreaterThan(TValue target, TValue lower, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(lower) <= 0) + { + throw new ArgumentException($"Value must be greater than {lower}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GreaterEquals(TValue target, TValue lower, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(lower) < 0) + { + throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LessThan(TValue target, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(upper) >= 0) + { + throw new ArgumentException($"Value must be less than {upper}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LessEquals(TValue target, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(upper) > 0) + { + throw new ArgumentException($"Value must be less or equal to {upper}", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotEmpty(IReadOnlyCollection? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNull(target, parameterName); + + if (target != null && target.Count == 0) + { + throw new ArgumentException("Collection does not contain an item.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotEmpty(Guid target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotNull(object? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == null) + { + throw new ArgumentNullException(parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotDefault(T target, [CallerArgumentExpression("target")] string? parameterName = null) + { + if (Equals(target, default(T)!)) + { + throw new ArgumentException("Value cannot be an the default value.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotNullOrEmpty(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNull(target, parameterName); + + if (string.IsNullOrWhiteSpace(target)) + { + throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidFileName(string? target, [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); + + if (target.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + throw new ArgumentException("Value contains an invalid character.", parameterName); + } + } + + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Valid(IValidatable? target, [CallerArgumentExpression("target")] string parameterName, Func message) + { + NotNull(target, parameterName); + + target?.Validate(message); + } + } +} diff --git a/src/Squidex.Infrastructure/HashSet.cs b/backend/src/Squidex.Infrastructure/HashSet.cs similarity index 100% rename from src/Squidex.Infrastructure/HashSet.cs rename to backend/src/Squidex.Infrastructure/HashSet.cs diff --git a/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs new file mode 100644 index 000000000..24bcd0c29 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace Squidex.Infrastructure.Http +{ + public static class DumpFormatter + { + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? responseBody) + { + return BuildDump(request, response, null, responseBody, TimeSpan.Zero); + } + + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody) + { + return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); + } + + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody, TimeSpan elapsed, bool isTimeout = false) + { + var writer = new StringBuilder(); + + writer.AppendLine("Request:"); + writer.AppendRequest(request, requestBody); + + writer.AppendLine(); + writer.AppendLine(); + + writer.AppendLine("Response:"); + writer.AppendResponse(response, responseBody, elapsed, isTimeout); + + return writer.ToString(); + } + + private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string? requestBody) + { + var method = request.Method.ToString().ToUpperInvariant(); + + writer.AppendLine($"{method}: {request.RequestUri} HTTP/{request.Version}"); + + writer.AppendHeaders(request.Headers); + writer.AppendHeaders(request.Content?.Headers); + + if (!string.IsNullOrWhiteSpace(requestBody)) + { + writer.AppendLine(); + writer.AppendLine(requestBody); + } + } + + private static void AppendResponse(this StringBuilder writer, HttpResponseMessage? response, string? responseBody, TimeSpan elapsed, bool isTimeout) + { + if (response != null) + { + var responseCode = (int)response.StatusCode; + var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); + + writer.AppendLine($"HTTP/{response.Version} {responseCode} {responseText}"); + + writer.AppendHeaders(response.Headers); + writer.AppendHeaders(response.Content?.Headers); + } + + if (!string.IsNullOrWhiteSpace(responseBody)) + { + writer.AppendLine(); + writer.AppendLine(responseBody); + } + + if (response != null && elapsed != TimeSpan.Zero) + { + writer.AppendLine(); + writer.AppendLine($"Elapsed: {elapsed}"); + } + + if (isTimeout) + { + writer.AppendLine($"Timeout after {elapsed}"); + } + } + + private static void AppendHeaders(this StringBuilder writer, HttpHeaders? headers) + { + if (headers == null) + { + return; + } + + foreach (var header in headers) + { + writer.Append(header.Key); + writer.Append(": "); + writer.Append(string.Join("; ", header.Value)); + writer.AppendLine(); + } + } + } +} diff --git a/src/Squidex.Infrastructure/IBackgroundProcess.cs b/backend/src/Squidex.Infrastructure/IBackgroundProcess.cs similarity index 100% rename from src/Squidex.Infrastructure/IBackgroundProcess.cs rename to backend/src/Squidex.Infrastructure/IBackgroundProcess.cs diff --git a/src/Squidex.Infrastructure/IFreezable.cs b/backend/src/Squidex.Infrastructure/IFreezable.cs similarity index 100% rename from src/Squidex.Infrastructure/IFreezable.cs rename to backend/src/Squidex.Infrastructure/IFreezable.cs diff --git a/src/Squidex.Infrastructure/IInitializable.cs b/backend/src/Squidex.Infrastructure/IInitializable.cs similarity index 100% rename from src/Squidex.Infrastructure/IInitializable.cs rename to backend/src/Squidex.Infrastructure/IInitializable.cs diff --git a/src/Squidex.Infrastructure/IResultList.cs b/backend/src/Squidex.Infrastructure/IResultList.cs similarity index 100% rename from src/Squidex.Infrastructure/IResultList.cs rename to backend/src/Squidex.Infrastructure/IResultList.cs diff --git a/src/Squidex.Infrastructure/InstantExtensions.cs b/backend/src/Squidex.Infrastructure/InstantExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/InstantExtensions.cs rename to backend/src/Squidex.Infrastructure/InstantExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs new file mode 100644 index 000000000..c544520ed --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; + +namespace Squidex.Infrastructure.Json +{ + public interface IJsonSerializer + { + string Serialize(T value, bool intented = false); + + void Serialize(T value, Stream stream); + + T Deserialize(string value, Type? actualType = null, Func? stringConverter = null); + + T Deserialize(Stream stream, Type? actualType = null, Func? stringConverter = null); + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/ClaimsPrincipalConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs new file mode 100644 index 000000000..dced0314d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver + { + private readonly JsonConverter[] converters; + private readonly object lockObject = new object(); + private Dictionary converterCache = new Dictionary(); + + public ConverterContractResolver(params JsonConverter[] converters) + { + NamingStrategy = new CamelCaseNamingStrategy(false, true); + + this.converters = converters; + + foreach (var converter in converters) + { + if (converter is ISupportedTypes supportedTypes) + { + foreach (var type in supportedTypes.SupportedTypes) + { + converterCache[type] = converter; + } + } + } + } + + protected override JsonArrayContract CreateArrayContract(Type objectType) + { + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) + { + var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments()); + + return base.CreateArrayContract(implementationType); + } + + return base.CreateArrayContract(objectType); + } + + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) + { + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + { + var implementationType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments()); + + return base.CreateDictionaryContract(implementationType); + } + + return base.CreateDictionaryContract(objectType); + } + + protected override JsonConverter? ResolveContractConverter(Type objectType) + { + JsonConverter? result = base.ResolveContractConverter(objectType); + + if (result != null) + { + return result; + } + + var cache = converterCache; + + if (cache == null || !cache.TryGetValue(objectType, out result)) + { + foreach (var converter in converters) + { + if (converter.CanConvert(objectType)) + { + result = converter; + } + } + + lock (lockObject) + { + cache = converterCache; + + var updatedCache = (cache != null) + ? new Dictionary(cache) + : new Dictionary(); + updatedCache[objectType] = result; + + converterCache = updatedCache; + } + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/EnvelopeHeadersConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/ISupportedTypes.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs new file mode 100644 index 000000000..8b405f652 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using NodaTime; +using NodaTime.Text; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class InstantConverter : JsonConverter + { + public IEnumerable SupportedTypes + { + get + { + yield return typeof(Instant); + yield return typeof(Instant?); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + writer.WriteValue(value.ToString()); + } + else + { + writer.WriteNull(); + } + } + + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + return InstantPattern.General.Parse(reader.Value.ToString()).Value; + } + + if (reader.TokenType == JsonToken.Date) + { + return Instant.FromDateTimeUtc((DateTime)reader.Value); + } + + if (reader.TokenType == JsonToken.Null && objectType == typeof(Instant?)) + { + return null; + } + + throw new JsonException($"Not a valid date time, expected String or Date, but got {reader.TokenType}."); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Instant) || objectType == typeof(Instant?); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs new file mode 100644 index 000000000..1709db763 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public abstract class JsonClassConverter : JsonConverter, ISupportedTypes where T : class + { + public virtual IEnumerable SupportedTypes + { + get { yield return typeof(T); } + } + + public sealed override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + return ReadValue(reader, objectType, serializer); + } + + protected abstract T ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer); + + public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + WriteValue(writer, (T)value, serializer); + } + + protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs new file mode 100644 index 000000000..88231ce51 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs @@ -0,0 +1,184 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Objects; + +#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public class JsonValueConverter : JsonConverter, ISupportedTypes + { + private readonly HashSet supportedTypes = new HashSet + { + typeof(IJsonValue), + typeof(JsonArray), + typeof(JsonBoolean), + typeof(JsonNull), + typeof(JsonNumber), + typeof(JsonObject), + typeof(JsonString) + }; + + public virtual IEnumerable SupportedTypes + { + get { return supportedTypes; } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return ReadJson(reader); + } + + private static IJsonValue ReadJson(JsonReader reader) + { + switch (reader.TokenType) + { + case JsonToken.Comment: + reader.Read(); + break; + case JsonToken.StartObject: + { + var result = JsonValue.Object(); + + 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 = ReadJson(reader); + + result[propertyName] = value; + break; + case JsonToken.EndObject: + return result; + } + } + + throw new JsonSerializationException("Unexpected end when reading Object."); + } + + case JsonToken.StartArray: + { + var result = JsonValue.Array(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.Comment: + break; + default: + var value = ReadJson(reader); + + result.Add(value); + break; + case JsonToken.EndArray: + return result; + } + } + + throw new JsonSerializationException("Unexpected end when reading Object."); + } + + case JsonToken.Integer: + return JsonValue.Create((long)reader.Value); + case JsonToken.Float: + return JsonValue.Create((double)reader.Value); + case JsonToken.Boolean: + return JsonValue.Create((bool)reader.Value); + case JsonToken.Date: + return JsonValue.Create(((DateTime)reader.Value).ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); + case JsonToken.String: + return JsonValue.Create(reader.Value.ToString()); + case JsonToken.Null: + case JsonToken.Undefined: + return JsonValue.Null; + } + + throw new NotSupportedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + WriteJson(writer, (IJsonValue)value); + } + + private static void WriteJson(JsonWriter writer, IJsonValue value) + { + switch (value) + { + case JsonNull _: + writer.WriteNull(); + break; + case JsonBoolean s: + writer.WriteValue(s.Value); + break; + case JsonString s: + writer.WriteValue(s.Value); + break; + case JsonNumber s: + + if (s.Value % 1 == 0) + { + writer.WriteValue((long)s.Value); + } + else + { + writer.WriteValue(s.Value); + } + + break; + case JsonArray array: + writer.WriteStartArray(); + + for (var i = 0; i < array.Count; i++) + { + WriteJson(writer, array[i]); + } + + writer.WriteEndArray(); + break; + + case JsonObject obj: + writer.WriteStartObject(); + + foreach (var kvp in obj) + { + writer.WritePropertyName(kvp.Key); + + WriteJson(writer, kvp.Value); + } + + writer.WriteEndObject(); + break; + } + } + + public override bool CanConvert(Type objectType) + { + return supportedTypes.Contains(objectType); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs new file mode 100644 index 000000000..7a02a55a3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class NewtonsoftJsonSerializer : IJsonSerializer + { + private readonly JsonSerializerSettings settings; + private readonly JsonSerializer serializer; + + private sealed class CustomReader : JsonTextReader + { + private readonly Func stringConverter; + + public override object Value + { + get + { + var value = base.Value; + + if (value is string s) + { + return stringConverter(s); + } + + return value; + } + } + + public CustomReader(TextReader reader, Func stringConverter) + : base(reader) + { + this.stringConverter = stringConverter; + } + } + + public NewtonsoftJsonSerializer(JsonSerializerSettings settings) + { + Guard.NotNull(settings); + + this.settings = settings; + + serializer = JsonSerializer.Create(settings); + } + + public string Serialize(T value, bool intented) + { + return JsonConvert.SerializeObject(value, intented ? Formatting.Indented : Formatting.None, settings); + } + + public void Serialize(T value, Stream stream) + { + using (var writer = new StreamWriter(stream)) + { + serializer.Serialize(writer, value); + + writer.Flush(); + } + } + + public T Deserialize(string value, Type? actualType = null, Func? stringConverter = null) + { + using (var textReader = new StringReader(value)) + { + actualType ??= typeof(T); + + using (var reader = GetReader(stringConverter, textReader)) + { + return (T)serializer.Deserialize(reader, actualType); + } + } + } + + public T Deserialize(Stream stream, Type? actualType = null, Func? stringConverter = null) + { + using (var textReader = new StreamReader(stream)) + { + actualType ??= typeof(T); + + using (var reader = GetReader(stringConverter, textReader)) + { + return (T)serializer.Deserialize(reader, actualType); + } + } + } + + private static JsonTextReader GetReader(Func? stringConverter, TextReader textReader) + { + return stringConverter != null ? new CustomReader(textReader, stringConverter) : new JsonTextReader(textReader); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs rename to backend/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs new file mode 100644 index 000000000..d3dce95b6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json.Serialization; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.Json.Newtonsoft +{ + public sealed class TypeNameSerializationBinder : DefaultSerializationBinder + { + private readonly TypeNameRegistry typeNameRegistry; + + public TypeNameSerializationBinder(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry); + + this.typeNameRegistry = typeNameRegistry; + } + + public override Type BindToType(string assemblyName, string typeName) + { + var type = typeNameRegistry.GetTypeOrNull(typeName); + + return type ?? base.BindToType(assemblyName, typeName); + } + + public override void BindToName(Type serializedType, out string? assemblyName, out string typeName) + { + assemblyName = null; + + var name = typeNameRegistry.GetNameOrNull(serializedType); + + if (name != null) + { + typeName = name; + } + else + { + base.BindToName(serializedType, out assemblyName, out typeName); + } + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs new file mode 100644 index 000000000..34596e2d2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Json.Objects +{ + public interface IJsonValue : IEquatable + { + JsonValueType Type { get; } + + string ToJsonString(); + + string ToString(); + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs new file mode 100644 index 000000000..efbfffdeb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Squidex.Infrastructure.Json.Objects +{ + public sealed class JsonArray : Collection, IJsonValue, IEquatable + { + public JsonValueType Type + { + get { return JsonValueType.Array; } + } + + public JsonArray() + { + } + + internal JsonArray(params object?[] values) + : base(ToList(values)) + { + } + + private static List ToList(IEnumerable values) + { + return values?.Select(JsonValue.Create).ToList() ?? new List(); + } + + protected override void InsertItem(int index, IJsonValue item) + { + base.InsertItem(index, item ?? JsonValue.Null); + } + + protected override void SetItem(int index, IJsonValue item) + { + base.SetItem(index, item ?? JsonValue.Null); + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonArray); + } + + public bool Equals(IJsonValue? other) + { + return Equals(other as JsonArray); + } + + public bool Equals(JsonArray? array) + { + if (array == null || array.Count != Count) + { + return false; + } + + for (var i = 0; i < Count; i++) + { + if (!this[i].Equals(array[i])) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + var hashCode = 17; + + for (var i = 0; i < Count; i++) + { + hashCode = (hashCode * 23) + this[i].GetHashCode(); + } + + return hashCode; + } + + public string ToJsonString() + { + return ToString(); + } + + public override string ToString() + { + return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonBoolean.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs new file mode 100644 index 000000000..8d8cfe754 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Json.Objects +{ + public sealed class JsonNull : IJsonValue, IEquatable + { + public static readonly JsonNull Null = new JsonNull(); + + public JsonValueType Type + { + get { return JsonValueType.Null; } + } + + private JsonNull() + { + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonNull); + } + + public bool Equals(IJsonValue? other) + { + return Equals(other as JsonNull); + } + + public bool Equals(JsonNull? other) + { + return other != null; + } + + public override int GetHashCode() + { + return 0; + } + + public string ToJsonString() + { + return ToString(); + } + + public override string ToString() + { + return "null"; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonNumber.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs new file mode 100644 index 000000000..29dfa2b05 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// 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.Infrastructure.Json.Objects +{ + public class JsonObject : IReadOnlyDictionary, IJsonValue, IEquatable + { + private readonly Dictionary inner; + + public IJsonValue this[string key] + { + get + { + return inner[key]; + } + set + { + Guard.NotNull(key); + + inner[key] = value ?? JsonValue.Null; + } + } + + public IEnumerable Keys + { + get { return inner.Keys; } + } + + public IEnumerable Values + { + get { return inner.Values; } + } + + public int Count + { + get { return inner.Count; } + } + + public JsonValueType Type + { + get { return JsonValueType.Array; } + } + + internal JsonObject() + { + inner = new Dictionary(); + } + + public JsonObject(JsonObject obj) + { + inner = new Dictionary(obj.inner); + } + + public JsonObject Add(string key, object? value) + { + return Add(key, JsonValue.Create(value)); + } + + public JsonObject Add(string key, IJsonValue? value) + { + inner[key] = value ?? JsonValue.Null; + + return this; + } + + public void Clear() + { + inner.Clear(); + } + + public bool Remove(string key) + { + return inner.Remove(key); + } + + public bool ContainsKey(string key) + { + return inner.ContainsKey(key); + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out IJsonValue value) + { + return inner.TryGetValue(key, out value!); + } + + public IEnumerator> GetEnumerator() + { + return inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return inner.GetEnumerator(); + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonObject); + } + + public bool Equals(IJsonValue other) + { + return Equals(other as JsonObject); + } + + public bool Equals(JsonObject? other) + { + return other != null && inner.EqualsDictionary(other.inner); + } + + public override int GetHashCode() + { + return inner.DictionaryHashCode(); + } + + public string ToJsonString() + { + return ToString(); + } + + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs new file mode 100644 index 000000000..8b0ab1c12 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Json.Objects +{ + public abstract class JsonScalar : IJsonValue, IEquatable> where T : notnull + { + public abstract JsonValueType Type { get; } + + public T Value { get; } + + protected JsonScalar(T value) + { + Value = value; + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonScalar); + } + + public bool Equals(IJsonValue? other) + { + return Equals(other as JsonScalar); + } + + public bool Equals(JsonScalar? other) + { + return other != null && other.Type == Type && Equals(other.Value, Value); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public override string ToString() + { + return Value.ToString()!; + } + + public virtual string ToJsonString() + { + return ToString(); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonString.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonString.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonString.cs diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs new file mode 100644 index 000000000..dfe024aad --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; + +#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator + +namespace Squidex.Infrastructure.Json.Objects +{ + public static class JsonValue + { + public static readonly IJsonValue Empty = new JsonString(string.Empty); + + public static readonly IJsonValue True = JsonBoolean.True; + public static readonly IJsonValue False = JsonBoolean.False; + + public static readonly IJsonValue Null = JsonNull.Null; + + public static readonly IJsonValue Zero = new JsonNumber(0); + + public static JsonArray Array() + { + return new JsonArray(); + } + + public static JsonArray Array(params object?[] values) + { + return new JsonArray(values); + } + + public static JsonObject Object() + { + return new JsonObject(); + } + + public static IJsonValue Create(object? value) + { + if (value == null) + { + return Null; + } + + if (value is IJsonValue v) + { + return v; + } + + switch (value) + { + case string s: + return Create(s); + case bool b: + return Create(b); + case float f: + return Create(f); + case double d: + return Create(d); + case int i: + return Create(i); + case long l: + return Create(l); + case Instant i: + return Create(i); + } + + throw new ArgumentException("Invalid json type"); + } + + public static IJsonValue Create(bool value) + { + return value ? True : False; + } + + public static IJsonValue Create(double value) + { + Guard.ValidNumber(value); + + if (value == 0) + { + return Zero; + } + + return new JsonNumber(value); + } + + public static IJsonValue Create(Instant? value) + { + if (value == null) + { + return Null; + } + + return Create(value.Value.ToString()); + } + + public static IJsonValue Create(double? value) + { + if (value == null) + { + return Null; + } + + return Create(value.Value); + } + + public static IJsonValue Create(bool? value) + { + if (value == null) + { + return Null; + } + + return Create(value.Value); + } + + public static IJsonValue Create(string? value) + { + if (value == null) + { + return Null; + } + + if (value.Length == 0) + { + return Empty; + } + + return new JsonString(value); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs similarity index 100% rename from src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs rename to backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs diff --git a/backend/src/Squidex.Infrastructure/Language.cs b/backend/src/Squidex.Infrastructure/Language.cs new file mode 100644 index 000000000..9a86fb6ba --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Language.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// 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.Text.RegularExpressions; + +namespace Squidex.Infrastructure +{ + public sealed partial class Language + { + private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); + private static readonly Dictionary AllLanguagesField = new Dictionary(StringComparer.OrdinalIgnoreCase); + + internal static Language AddLanguage(string iso2Code, string englishName) + { + return AllLanguagesField.GetOrAdd(iso2Code, englishName, (c, n) => new Language(c, n)); + } + + public static Language GetLanguage(string iso2Code) + { + Guard.NotNullOrEmpty(iso2Code); + + try + { + return AllLanguagesField[iso2Code]; + } + catch (KeyNotFoundException) + { + throw new NotSupportedException($"Language {iso2Code} is not supported"); + } + } + + public static IReadOnlyCollection AllLanguages + { + get { return AllLanguagesField.Values; } + } + + public string EnglishName { get; } + + public string Iso2Code { get; } + + private Language(string iso2Code, string englishName) + { + Iso2Code = iso2Code; + + EnglishName = englishName; + } + + public static bool IsValidLanguage(string iso2Code) + { + Guard.NotNullOrEmpty(iso2Code); + + return AllLanguagesField.ContainsKey(iso2Code); + } + + public static bool TryGetLanguage(string iso2Code, [MaybeNullWhen(false)] out Language language) + { + Guard.NotNullOrEmpty(iso2Code); + + return AllLanguagesField.TryGetValue(iso2Code, out language!); + } + + public static implicit operator string(Language language) + { + return language.Iso2Code; + } + + public static implicit operator Language(string iso2Code) + { + return GetLanguage(iso2Code!); + } + + public static Language? ParseOrNull(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return null; + } + + input = input.Trim(); + + if (input.Length != 2) + { + var match = CultureRegex.Match(input); + + if (!match.Success) + { + return null; + } + + input = match.Groups[1].Value; + } + + if (TryGetLanguage(input.ToLowerInvariant(), out var result)) + { + return result; + } + + return null; + } + + public override string ToString() + { + return EnglishName; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Languages.cs b/backend/src/Squidex.Infrastructure/Languages.cs similarity index 100% rename from src/Squidex.Infrastructure/Languages.cs rename to backend/src/Squidex.Infrastructure/Languages.cs diff --git a/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs new file mode 100644 index 000000000..d2ce13965 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure +{ + public sealed class LanguagesInitializer : IInitializable + { + private readonly LanguagesOptions options; + + public LanguagesInitializer(IOptions options) + { + Guard.NotNull(options); + + this.options = options.Value; + } + + public Task InitializeAsync(CancellationToken ct = default) + { + foreach (var kvp in options) + { + if (!string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value)) + { + Language.AddLanguage(kvp.Key, kvp.Value); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Infrastructure/LanguagesOptions.cs b/backend/src/Squidex.Infrastructure/LanguagesOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/LanguagesOptions.cs rename to backend/src/Squidex.Infrastructure/LanguagesOptions.cs diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs rename to backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs rename to backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs new file mode 100644 index 000000000..931f3312f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Squidex.Infrastructure.Log.Adapter +{ + public class SemanticLogLoggerProvider : ILoggerProvider + { + private readonly IServiceProvider services; + private ISemanticLog? log; + + public SemanticLogLoggerProvider(IServiceProvider services) + { + Guard.NotNull(services); + + this.services = services; + } + + internal SemanticLogLoggerProvider(ISemanticLog? log) + { + this.log = log; + } + + public static SemanticLogLoggerProvider ForTesting(ISemanticLog? log) + { + return new SemanticLogLoggerProvider(log); + } + + public ILogger CreateLogger(string categoryName) + { + if (log == null && services != null) + { + log = services.GetService(typeof(ISemanticLog)) as ISemanticLog; + } + + if (log == null) + { + return NullLogger.Instance; + } + + return new SemanticLogLogger(log.CreateScope(writer => + { + writer.WriteProperty("category", categoryName); + })); + } + + public void Dispose() + { + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs new file mode 100644 index 000000000..55af0fa92 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Reflection; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ApplicationInfoLogAppender : ILogAppender + { + private readonly string applicationName; + private readonly string applicationVersion; + private readonly string applicationSessionId; + + public ApplicationInfoLogAppender(Type type, Guid applicationSession) + : this(type?.Assembly!, applicationSession) + { + } + + public ApplicationInfoLogAppender(Assembly assembly, Guid applicationSession) + { + Guard.NotNull(assembly); + + applicationName = assembly.GetName().Name!; + applicationVersion = assembly.GetName().Version!.ToString()!; + applicationSessionId = applicationSession.ToString(); + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel) + { + writer.WriteObject("app", w => w + .WriteProperty("name", applicationName) + .WriteProperty("version", applicationVersion) + .WriteProperty("sessionId", applicationSessionId)); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs b/backend/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs rename to backend/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs diff --git a/backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs b/backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs new file mode 100644 index 000000000..4fcb839ab --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ConstantsLogWriter : ILogAppender + { + private readonly Action objectWriter; + + public ConstantsLogWriter(Action objectWriter) + { + Guard.NotNull(objectWriter); + + this.objectWriter = objectWriter; + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel) + { + objectWriter(writer); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/DebugLogChannel.cs b/backend/src/Squidex.Infrastructure/Log/DebugLogChannel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/DebugLogChannel.cs rename to backend/src/Squidex.Infrastructure/Log/DebugLogChannel.cs diff --git a/backend/src/Squidex.Infrastructure/Log/FileChannel.cs b/backend/src/Squidex.Infrastructure/Log/FileChannel.cs new file mode 100644 index 000000000..897aa0693 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/FileChannel.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Log.Internal; + +namespace Squidex.Infrastructure.Log +{ + public sealed class FileChannel : DisposableObjectBase, ILogChannel + { + private readonly FileLogProcessor processor; + private readonly object lockObject = new object(); + private volatile bool isInitialized; + + public FileChannel(string path) + { + Guard.NotNullOrEmpty(path); + + processor = new FileLogProcessor(path); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + processor.Dispose(); + } + } + + public void Log(SemanticLogLevel logLevel, string message) + { + if (!isInitialized) + { + lock (lockObject) + { + if (!isInitialized) + { + processor.Initialize(); + + isInitialized = true; + } + } + } + + processor.EnqueueMessage(new LogMessageEntry { Message = message }); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/IArrayWriter.cs b/backend/src/Squidex.Infrastructure/Log/IArrayWriter.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/IArrayWriter.cs rename to backend/src/Squidex.Infrastructure/Log/IArrayWriter.cs diff --git a/src/Squidex.Infrastructure/Log/ILogAppender.cs b/backend/src/Squidex.Infrastructure/Log/ILogAppender.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ILogAppender.cs rename to backend/src/Squidex.Infrastructure/Log/ILogAppender.cs diff --git a/src/Squidex.Infrastructure/Log/ILogChannel.cs b/backend/src/Squidex.Infrastructure/Log/ILogChannel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ILogChannel.cs rename to backend/src/Squidex.Infrastructure/Log/ILogChannel.cs diff --git a/src/Squidex.Infrastructure/Log/ILogStore.cs b/backend/src/Squidex.Infrastructure/Log/ILogStore.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ILogStore.cs rename to backend/src/Squidex.Infrastructure/Log/ILogStore.cs diff --git a/backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs b/backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs new file mode 100644 index 000000000..5f4e2b5cb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/IObjectWriter.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Infrastructure.Log +{ + public interface IObjectWriter + { + IObjectWriter WriteProperty(string property, string? value); + + IObjectWriter WriteProperty(string property, double value); + + IObjectWriter WriteProperty(string property, long value); + + IObjectWriter WriteProperty(string property, bool value); + + IObjectWriter WriteProperty(string property, TimeSpan value); + + IObjectWriter WriteProperty(string property, Instant value); + + IObjectWriter WriteObject(string property, Action objectWriter); + + IObjectWriter WriteObject(string property, T context, Action objectWriter); + + IObjectWriter WriteArray(string property, Action arrayWriter); + + IObjectWriter WriteArray(string property, T context, Action arrayWriter); + + string ToString(); + } +} diff --git a/src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs b/backend/src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs rename to backend/src/Squidex.Infrastructure/Log/IObjectWriterFactory.cs diff --git a/src/Squidex.Infrastructure/Log/ISemanticLog.cs b/backend/src/Squidex.Infrastructure/Log/ISemanticLog.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/ISemanticLog.cs rename to backend/src/Squidex.Infrastructure/Log/ISemanticLog.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs b/backend/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs diff --git a/backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs b/backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs new file mode 100644 index 000000000..a18c4af1a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Squidex.Infrastructure.Log.Internal +{ + public sealed class ConsoleLogProcessor : DisposableObjectBase + { + private const int MaxQueuedMessages = 1024; + private readonly IConsole console; + private readonly BlockingCollection messageQueue = new BlockingCollection(MaxQueuedMessages); + private readonly Thread outputThread; + + public ConsoleLogProcessor() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + console = new WindowsLogConsole(true); + } + else + { + console = new AnsiLogConsole(false); + } + + outputThread = new Thread(ProcessLogQueue) + { + IsBackground = true, Name = "Logging" + }; + + outputThread.Start(); + } + + public void EnqueueMessage(LogMessageEntry message) + { + if (!messageQueue.IsAddingCompleted) + { + try + { + messageQueue.Add(message); + return; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to enqueue log message: {ex}."); + } + } + + WriteMessage(message); + } + + private void ProcessLogQueue() + { + try + { + foreach (var message in messageQueue.GetConsumingEnumerable()) + { + WriteMessage(message); + } + } + catch + { + try + { + messageQueue.CompleteAdding(); + } + catch + { + return; + } + } + } + + private void WriteMessage(LogMessageEntry entry) + { + console.WriteLine(entry.Color, entry.Message); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + messageQueue.CompleteAdding(); + + try + { + outputThread.Join(1500); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to shutdown log queue grateful: {ex}."); + } + finally + { + console.Reset(); + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs b/backend/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/IConsole.cs b/backend/src/Squidex.Infrastructure/Log/Internal/IConsole.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/IConsole.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/IConsole.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs b/backend/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs diff --git a/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs b/backend/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs rename to backend/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs diff --git a/backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs b/backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs new file mode 100644 index 000000000..aa28689d8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/JsonLogWriter.cs @@ -0,0 +1,225 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using NodaTime; + +namespace Squidex.Infrastructure.Log +{ + public sealed class JsonLogWriter : IObjectWriter, IArrayWriter + { + private readonly JsonWriterOptions formatting; + private readonly bool formatLine; + private readonly MemoryStream stream = new MemoryStream(); + private readonly StreamReader streamReader; + private Utf8JsonWriter jsonWriter; + + public long BufferSize + { + get { return stream.Length; } + } + + internal JsonLogWriter(JsonWriterOptions formatting, bool formatLine) + { + this.formatLine = formatLine; + this.formatting = formatting; + + streamReader = new StreamReader(stream, Encoding.UTF8); + + Start(); + } + + private void Start() + { + jsonWriter = new Utf8JsonWriter(stream, formatting); + jsonWriter.WriteStartObject(); + } + + internal void Reset() + { + stream.Position = 0; + stream.SetLength(0); + + Start(); + } + + IArrayWriter IArrayWriter.WriteValue(string value) + { + jsonWriter.WriteStringValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(double value) + { + jsonWriter.WriteNumberValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(long value) + { + jsonWriter.WriteNumberValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(bool value) + { + jsonWriter.WriteBooleanValue(value); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(Instant value) + { + jsonWriter.WriteStringValue(value.ToString()); + + return this; + } + + IArrayWriter IArrayWriter.WriteValue(TimeSpan value) + { + jsonWriter.WriteStringValue(value.ToString()); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, string? value) + { + jsonWriter.WriteString(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, double value) + { + jsonWriter.WriteNumber(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, long value) + { + jsonWriter.WriteNumber(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, bool value) + { + jsonWriter.WriteBoolean(property, value); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, Instant value) + { + jsonWriter.WriteString(property, value.ToString()); + + return this; + } + + IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value) + { + jsonWriter.WriteString(property, value.ToString()); + + return this; + } + + IObjectWriter IObjectWriter.WriteObject(string property, Action objectWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(this); + + jsonWriter.WriteEndObject(); + + return this; + } + + IObjectWriter IObjectWriter.WriteObject(string property, T context, Action objectWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(context, this); + + jsonWriter.WriteEndObject(); + + return this; + } + + IObjectWriter IObjectWriter.WriteArray(string property, Action arrayWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartArray(); + + arrayWriter?.Invoke(this); + + jsonWriter.WriteEndArray(); + + return this; + } + + IObjectWriter IObjectWriter.WriteArray(string property, T context, Action arrayWriter) + { + jsonWriter.WritePropertyName(property); + jsonWriter.WriteStartArray(); + + arrayWriter?.Invoke(context, this); + + jsonWriter.WriteEndArray(); + + return this; + } + + IArrayWriter IArrayWriter.WriteObject(Action objectWriter) + { + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(this); + + jsonWriter.WriteEndObject(); + + return this; + } + + IArrayWriter IArrayWriter.WriteObject(T context, Action objectWriter) + { + jsonWriter.WriteStartObject(); + + objectWriter?.Invoke(context, this); + + jsonWriter.WriteEndObject(); + + return this; + } + + public override string ToString() + { + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + + stream.Position = 0; + streamReader.DiscardBufferedData(); + + var json = streamReader.ReadToEnd(); + + if (formatLine) + { + json += Environment.NewLine; + } + + return json; + } + } +} diff --git a/src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs b/backend/src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs rename to backend/src/Squidex.Infrastructure/Log/JsonLogWriterFactory.cs diff --git a/backend/src/Squidex.Infrastructure/Log/LockingLogStore.cs b/backend/src/Squidex.Infrastructure/Log/LockingLogStore.cs new file mode 100644 index 000000000..069e8329c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/LockingLogStore.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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.Log +{ + public sealed class LockingLogStore : ILogStore + { + private static readonly byte[] LockedText = Encoding.UTF8.GetBytes("Another process is currenty running, try it again later."); + private static readonly TimeSpan LockWaitingTime = TimeSpan.FromMinutes(1); + private readonly ILogStore inner; + private readonly ILockGrain lockGrain; + + public LockingLogStore(ILogStore inner, IGrainFactory grainFactory) + { + Guard.NotNull(inner); + Guard.NotNull(grainFactory); + + this.inner = inner; + + lockGrain = grainFactory.GetGrain(SingleGrain.Id); + } + + public Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream) + { + return ReadLogAsync(key, from, to, stream, LockWaitingTime); + } + + public async Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream, TimeSpan lockTimeout) + { + using (var cts = new CancellationTokenSource(lockTimeout)) + { + string? releaseToken = null; + + while (!cts.IsCancellationRequested) + { + releaseToken = await lockGrain.AcquireLockAsync(key); + + if (releaseToken != null) + { + break; + } + + try + { + await Task.Delay(2000, cts.Token); + } + catch (OperationCanceledException) + { + break; + } + } + + if (releaseToken != null) + { + try + { + await inner.ReadLogAsync(key, from, to, stream); + } + finally + { + await lockGrain.ReleaseLockAsync(releaseToken); + } + } + else + { + await stream.WriteAsync(LockedText, 0, LockedText.Length); + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/Log/NoopDisposable.cs b/backend/src/Squidex.Infrastructure/Log/NoopDisposable.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/NoopDisposable.cs rename to backend/src/Squidex.Infrastructure/Log/NoopDisposable.cs diff --git a/src/Squidex.Infrastructure/Log/NoopLogStore.cs b/backend/src/Squidex.Infrastructure/Log/NoopLogStore.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/NoopLogStore.cs rename to backend/src/Squidex.Infrastructure/Log/NoopLogStore.cs diff --git a/backend/src/Squidex.Infrastructure/Log/Profiler.cs b/backend/src/Squidex.Infrastructure/Log/Profiler.cs new file mode 100644 index 000000000..87b36515b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/Profiler.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Log +{ + public delegate void ProfilerStarted(ProfilerSpan span); + + public static class Profiler + { + private static readonly AsyncLocal LocalSession = new AsyncLocal(); + private static readonly AsyncLocalCleaner Cleaner; + + public static ProfilerSession? Session + { + get { return LocalSession.Value; } + } + + public static event ProfilerStarted SpanStarted; + + static Profiler() + { + Cleaner = new AsyncLocalCleaner(LocalSession); + } + + public static IDisposable StartSession() + { + LocalSession.Value = new ProfilerSession(); + + return Cleaner; + } + + public static IDisposable TraceMethod(Type type, [CallerMemberName] string? memberName = null) + { + return Trace($"{type.Name}/{memberName}"); + } + + public static IDisposable TraceMethod([CallerMemberName] string? memberName = null) + { + return Trace($"{typeof(T).Name}/{memberName}"); + } + + public static IDisposable TraceMethod(string objectName, [CallerMemberName] string? memberName = null) + { + return Trace($"{objectName}/{memberName}"); + } + + public static IDisposable Trace(string key) + { + Guard.NotNull(key); + + var session = LocalSession.Value; + + if (session == null) + { + return NoopDisposable.Instance; + } + + var span = new ProfilerSpan(session, key); + + SpanStarted?.Invoke(span); + + return span; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs b/backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs new file mode 100644 index 000000000..e08add22d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ProfilerSession.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ProfilerSession + { + private struct ProfilerItem + { + public long Total; + public long Count; + } + + private readonly ConcurrentDictionary traces = new ConcurrentDictionary(); + + public void Measured(string name, long elapsed) + { + Guard.NotNullOrEmpty(name); + + traces.AddOrUpdate(name, x => + { + return new ProfilerItem { Total = elapsed, Count = 1 }; + }, + (x, result) => + { + result.Total += elapsed; + result.Count++; + + return result; + }); + } + + public void Write(IObjectWriter writer) + { + Guard.NotNull(writer); + + if (traces.Count > 0) + { + writer.WriteObject("profiler", p => + { + foreach (var kvp in traces) + { + p.WriteObject(kvp.Key, kvp.Value, (value, k) => k + .WriteProperty("elapsedMsTotal", value.Total) + .WriteProperty("elapsedMsAvg", value.Total / value.Count) + .WriteProperty("count", value.Count)); + } + }); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs b/backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs new file mode 100644 index 000000000..a47b877a4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/ProfilerSpan.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Log +{ + public sealed class ProfilerSpan : IDisposable + { + private readonly ProfilerSession session; + private readonly string key; + private ValueStopwatch watch = ValueStopwatch.StartNew(); + private List hooks; + + public string Key + { + get { return key; } + } + + public ProfilerSpan(ProfilerSession session, string key) + { + this.session = session; + + this.key = key; + } + + public void Listen(IDisposable hook) + { + Guard.NotNull(hook); + + if (hooks == null) + { + hooks = new List(1); + } + + hooks.Add(hook); + } + + public void Dispose() + { + var elapsedMs = watch.Stop(); + + session.Measured(key, elapsedMs); + + if (hooks != null) + { + for (var i = 0; i < hooks.Count; i++) + { + try + { + hooks[i].Dispose(); + } + catch + { + continue; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/SemanticLog.cs b/backend/src/Squidex.Infrastructure/Log/SemanticLog.cs new file mode 100644 index 000000000..b168e11c3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/SemanticLog.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// 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; + +namespace Squidex.Infrastructure.Log +{ + public sealed class SemanticLog : ISemanticLog + { + private readonly ILogChannel[] channels; + private readonly ILogAppender[] appenders; + private readonly IObjectWriterFactory writerFactory; + + public SemanticLog( + IEnumerable channels, + IEnumerable appenders, + IObjectWriterFactory writerFactory) + { + Guard.NotNull(channels); + Guard.NotNull(appenders); + Guard.NotNull(writerFactory); + + this.channels = channels.ToArray(); + this.appenders = appenders.ToArray(); + this.writerFactory = writerFactory; + } + + public void Log(SemanticLogLevel logLevel, T context, Action action) + { + Guard.NotNull(action); + + var formattedText = FormatText(logLevel, context, action); + + LogFormattedText(logLevel, formattedText); + } + + private void LogFormattedText(SemanticLogLevel logLevel, string formattedText) + { + List? exceptions = null; + + for (var i = 0; i < channels.Length; i++) + { + try + { + channels[i].Log(logLevel, formattedText); + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List(); + } + + exceptions.Add(ex); + } + } + + if (exceptions != null && exceptions.Count > 0) + { + throw new AggregateException("An error occurred while writing to logger(s).", exceptions); + } + } + + private string FormatText(SemanticLogLevel logLevel, T context, Action objectWriter) + { + var writer = writerFactory.Create(); + + try + { + writer.WriteProperty(nameof(logLevel), logLevel.ToString()); + + objectWriter(context, writer); + + for (var i = 0; i < appenders.Length; i++) + { + appenders[i].Append(writer, logLevel); + } + + return writer.ToString(); + } + finally + { + writerFactory.Release(writer); + } + } + + public ISemanticLog CreateScope(Action objectWriter) + { + return new SemanticLog(channels, appenders.Union(new ILogAppender[] { new ConstantsLogWriter(objectWriter) }).ToArray(), writerFactory); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs b/backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs new file mode 100644 index 000000000..606097fe6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs @@ -0,0 +1,194 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Log +{ + public static class SemanticLogExtensions + { + public static void LogTrace(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Trace, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogTrace(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Trace, context, objectWriter); + } + + public static void LogDebug(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Debug, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogDebug(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Debug, context, objectWriter); + } + + public static void LogInformation(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Information, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogInformation(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Information, context, objectWriter); + } + + public static void LogWarning(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Warning, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogWarning(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Warning, context, objectWriter); + } + + public static void LogWarning(this ISemanticLog log, Exception exception, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Warning, None.Value, (_, w) => w.WriteException(exception, objectWriter)); + } + + public static void LogWarning(this ISemanticLog log, Exception exception, T context, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Warning, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); + } + + public static void LogError(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Error, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogError(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Error, context, objectWriter); + } + + public static void LogError(this ISemanticLog log, Exception exception, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Error, None.Value, (_, w) => w.WriteException(exception, objectWriter)); + } + + public static void LogError(this ISemanticLog log, Exception exception, T context, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Error, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); + } + + public static void LogFatal(this ISemanticLog log, Action objectWriter) + { + log.Log(SemanticLogLevel.Fatal, None.Value, (_, w) => objectWriter(w)); + } + + public static void LogFatal(this ISemanticLog log, T context, Action objectWriter) + { + log.Log(SemanticLogLevel.Fatal, context, objectWriter); + } + + public static void LogFatal(this ISemanticLog log, Exception? exception, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Fatal, None.Value, (_, w) => w.WriteException(exception, objectWriter)); + } + + public static void LogFatal(this ISemanticLog log, Exception? exception, T context, Action? objectWriter = null) + { + log.Log(SemanticLogLevel.Fatal, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); + } + + private static void WriteException(this IObjectWriter writer, Exception? exception, Action? objectWriter) + { + objectWriter?.Invoke(writer); + + if (exception != null) + { + writer.WriteException(exception); + } + } + + private static void WriteException(this IObjectWriter writer, Exception? exception, T context, Action? objectWriter) + { + objectWriter?.Invoke(context, writer); + + if (exception != null) + { + writer.WriteException(exception); + } + } + + public static IObjectWriter WriteException(this IObjectWriter writer, Exception? exception) + { + if (exception == null) + { + return writer; + } + + return writer.WriteObject(nameof(exception), exception, (ctx, w) => + { + w.WriteProperty("type", ctx.GetType().FullName); + + if (ctx.Message != null) + { + w.WriteProperty("message", ctx.Message); + } + + if (ctx.StackTrace != null) + { + w.WriteProperty("stackTrace", ctx.StackTrace); + } + }); + } + + public static IDisposable MeasureTrace(this ISemanticLog log, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Trace, None.Value, (_, w) => objectWriter(w)); + } + + public static IDisposable MeasureTrace(this ISemanticLog log, T context, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Trace, context, objectWriter); + } + + public static IDisposable MeasureDebug(this ISemanticLog log, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Debug, None.Value, (_, w) => objectWriter(w)); + } + + public static IDisposable MeasureDebug(this ISemanticLog log, T context, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Debug, context, objectWriter); + } + + public static IDisposable MeasureInformation(this ISemanticLog log, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Information, None.Value, (_, w) => objectWriter(w)); + } + + public static IDisposable MeasureInformation(this ISemanticLog log, T context, Action objectWriter) + { + return log.Measure(SemanticLogLevel.Information, context, objectWriter); + } + + private static IDisposable Measure(this ISemanticLog log, SemanticLogLevel logLevel, T context, Action objectWriter) + { + var watch = ValueStopwatch.StartNew(); + + return new DelegateDisposable(() => + { + var elapsedMs = watch.Stop(); + + log.Log(logLevel, (Context: context, elapsedMs), (ctx, w) => + { + objectWriter?.Invoke(ctx.Context, w); + + w.WriteProperty("elapsedMs", elapsedMs); + }); + }); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/SemanticLogLevel.cs b/backend/src/Squidex.Infrastructure/Log/SemanticLogLevel.cs similarity index 100% rename from src/Squidex.Infrastructure/Log/SemanticLogLevel.cs rename to backend/src/Squidex.Infrastructure/Log/SemanticLogLevel.cs diff --git a/backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs new file mode 100644 index 000000000..a971e529e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; + +namespace Squidex.Infrastructure.Log +{ + public sealed class TimestampLogAppender : ILogAppender + { + private readonly IClock clock; + + public TimestampLogAppender(IClock? clock = null) + { + this.clock = clock ?? SystemClock.Instance; + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel) + { + writer.WriteProperty("timestamp", clock.GetCurrentInstant()); + } + } +} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrated.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs similarity index 100% rename from src/Squidex.Infrastructure/Migrations/IMigrated.cs rename to backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs diff --git a/src/Squidex.Infrastructure/Migrations/IMigration.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs similarity index 100% rename from src/Squidex.Infrastructure/Migrations/IMigration.cs rename to backend/src/Squidex.Infrastructure/Migrations/IMigration.cs diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs new file mode 100644 index 000000000..b714bbacb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Migrations +{ + public interface IMigrationPath + { + (int Version, IEnumerable? Migrations) GetNext(int version); + } +} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs similarity index 100% rename from src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs rename to backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs diff --git a/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs b/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs new file mode 100644 index 000000000..9767b903a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.Migrations +{ + [Serializable] + public class MigrationFailedException : Exception + { + public string Name { get; } + + public MigrationFailedException(string name) + : base(FormatException(name)) + { + Name = name; + } + + public MigrationFailedException(string name, Exception inner) + : base(FormatException(name), inner) + { + Name = name; + } + + protected MigrationFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Name = info.GetString(nameof(Name))!; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Name), Name); + } + + private static string FormatException(string name) + { + return $"Failed to run migration '{name}'"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs new file mode 100644 index 000000000..15a79e3a5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Migrations +{ + public sealed class Migrator + { + private readonly ISemanticLog log; + private readonly IMigrationStatus migrationStatus; + private readonly IMigrationPath migrationPath; + + public int LockWaitMs { get; set; } = 500; + + public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, ISemanticLog log) + { + Guard.NotNull(migrationStatus); + Guard.NotNull(migrationPath); + Guard.NotNull(log); + + this.migrationStatus = migrationStatus; + this.migrationPath = migrationPath; + + this.log = log; + } + + public async Task MigrateAsync(CancellationToken ct = default) + { + var version = 0; + + try + { + while (!await migrationStatus.TryLockAsync()) + { + log.LogInformation(w => w + .WriteProperty("action", "Migrate") + .WriteProperty("mesage", $"Waiting {LockWaitMs}ms to acquire lock.")); + + await Task.Delay(LockWaitMs); + } + + version = await migrationStatus.GetVersionAsync(); + + while (!ct.IsCancellationRequested) + { + var (newVersion, migrations) = migrationPath.GetNext(version); + + if (migrations == null || !migrations.Any()) + { + break; + } + + foreach (var migration in migrations) + { + var name = migration.GetType().ToString(); + + log.LogInformation(w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Started") + .WriteProperty("migrator", name)); + + try + { + using (log.MeasureInformation(w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Completed") + .WriteProperty("migrator", name))) + { + await migration.UpdateAsync(); + } + } + catch (Exception ex) + { + log.LogFatal(ex, w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Failed") + .WriteProperty("migrator", name)); + + throw new MigrationFailedException(name, ex); + } + } + + version = newVersion; + } + } + finally + { + await migrationStatus.UnlockAsync(version); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/NamedId.cs b/backend/src/Squidex.Infrastructure/NamedId.cs new file mode 100644 index 000000000..c582a77ad --- /dev/null +++ b/backend/src/Squidex.Infrastructure/NamedId.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public static class NamedId + { + public static NamedId Of(T id, string name) where T : notnull + { + return new NamedId(id, name); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/NamedId{T}.cs b/backend/src/Squidex.Infrastructure/NamedId{T}.cs new file mode 100644 index 000000000..42f15e61d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/NamedId{T}.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure +{ + public delegate bool Parser(string input, out T result); + + public sealed class NamedId : IEquatable> where T : notnull + { + private static readonly int GuidLength = Guid.Empty.ToString().Length; + + public T Id { get; } + + public string Name { get; } + + public NamedId(T id, string name) + { + Guard.NotNull(id); + Guard.NotNull(name); + + Id = id; + + Name = name; + } + + public override string ToString() + { + return $"{Id},{Name}"; + } + + public override bool Equals(object? obj) + { + return Equals(obj as NamedId); + } + + public bool Equals(NamedId? other) + { + return other != null && (ReferenceEquals(this, other) || (Id.Equals(other.Id) && Name.Equals(other.Name))); + } + + public override int GetHashCode() + { + return (Id.GetHashCode() * 397) ^ Name.GetHashCode(); + } + + public static bool TryParse(string value, Parser parser, [MaybeNullWhen(false)] out NamedId result) + { + if (value != null) + { + if (typeof(T) == typeof(Guid)) + { + if (value.Length > GuidLength + 1 && value[GuidLength] == ',') + { + if (parser(value.Substring(0, GuidLength), out var id)) + { + result = new NamedId(id, value.Substring(GuidLength + 1)); + + return true; + } + } + } + else + { + var index = value.IndexOf(','); + + if (index > 0 && index < value.Length - 1) + { + if (parser(value.Substring(0, index), out var id)) + { + result = new NamedId(id, value.Substring(index + 1)); + + return true; + } + } + } + } + + result = null!; + + return false; + } + + public static NamedId Parse(string value, Parser parser) + { + if (!TryParse(value, parser, out var result)) + { + throw new ArgumentException("Named id must have at least 2 parts divided by commata.", nameof(value)); + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Net/IPAddressComparer.cs b/backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs similarity index 100% rename from src/Squidex.Infrastructure/Net/IPAddressComparer.cs rename to backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs diff --git a/backend/src/Squidex.Infrastructure/None.cs b/backend/src/Squidex.Infrastructure/None.cs new file mode 100644 index 000000000..5f1564823 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/None.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public sealed class None + { + public static readonly Type Type = typeof(None); + + public static readonly None Value = new None(); + + private None() + { + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs new file mode 100644 index 000000000..f4f5e890c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Runtime; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class ActivationLimit : IActivationLimit, IDeactivater + { + private readonly IGrainActivationContext context; + private readonly IActivationLimiter limiter; + private int maxActivations; + + public ActivationLimit(IGrainActivationContext context, IActivationLimiter limiter) + { + Guard.NotNull(context); + Guard.NotNull(limiter); + + this.context = context; + this.limiter = limiter; + } + + public void ReportIAmAlive() + { + if (maxActivations > 0) + { + limiter.Register(context.GrainType, this, maxActivations); + } + } + + public void ReportIAmDead() + { + if (maxActivations > 0) + { + limiter.Unregister(context.GrainType, this); + } + } + + public void SetLimit(int activations, TimeSpan lifetime) + { + maxActivations = activations; + + context.ObservableLifecycle?.Subscribe("Limiter", GrainLifecycleStage.Activate, + ct => + { + var runtime = context.ActivationServices.GetRequiredService(); + + runtime.DelayDeactivation(context.GrainInstance, lifetime); + + ReportIAmAlive(); + + return TaskHelper.Done; + }, + ct => + { + ReportIAmDead(); + + return TaskHelper.Done; + }); + } + + void IDeactivater.Deactivate() + { + var runtime = context.ActivationServices.GetRequiredService(); + + runtime.DeactivateOnIdle(context.GrainInstance); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs rename to backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs diff --git a/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs rename to backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs new file mode 100644 index 000000000..e8b940255 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Core; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Orleans +{ + public abstract class GrainBase : Grain + { + protected GrainBase() + { + } + + protected GrainBase(IGrainIdentity? identity, IGrainRuntime? runtime) + : base(identity, runtime) + { + } + + public void ReportIAmAlive() + { + var limit = ServiceProvider.GetService(); + + limit?.ReportIAmAlive(); + } + + public void ReportIAmDead() + { + var limit = ServiceProvider.GetService(); + + limit?.ReportIAmDead(); + } + + protected void TryDelayDeactivation(TimeSpan timeSpan) + { + try + { + DelayDeactivation(timeSpan); + } + catch (InvalidOperationException) + { + } + } + + protected void TryDeactivateOnIdle() + { + try + { + DeactivateOnIdle(); + } + catch (InvalidOperationException) + { + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs new file mode 100644 index 000000000..16cc558db --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain + { + private const int NumTries = 10; + private readonly IGrainFactory grainFactory; + + public GrainBootstrap(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory); + + this.grainFactory = grainFactory; + } + + public async Task StartAsync(CancellationToken ct = default) + { + for (var i = 1; i <= NumTries; i++) + { + ct.ThrowIfCancellationRequested(); + try + { + var grain = grainFactory.GetGrain(SingleGrain.Id); + + await grain.ActivateAsync(); + + return; + } + catch (OrleansException) + { + if (i == NumTries) + { + throw; + } + } + } + } + + public override string ToString() + { + return typeof(T).ToString(); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs rename to backend/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainOfString.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/GrainOfString.cs rename to backend/src/Squidex.Infrastructure/Orleans/GrainOfString.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs new file mode 100644 index 000000000..6c07ee7f0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// 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 Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Runtime; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class GrainState : IGrainState where T : class, new() + { + private readonly IGrainActivationContext context; + private IPersistence persistence; + + public T Value { get; set; } = new T(); + + public long Version + { + get { return persistence.Version; } + } + + public GrainState(IGrainActivationContext context) + { + Guard.NotNull(context); + + this.context = context; + + context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); + } + + public Task SetupAsync(CancellationToken ct = default) + { + if (ct.IsCancellationRequested) + { + return Task.CompletedTask; + } + + if (context.GrainIdentity.PrimaryKeyString != null) + { + var store = context.ActivationServices.GetService>(); + + persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKeyString, ApplyState); + } + else + { + var store = context.ActivationServices.GetService>(); + + persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKey, ApplyState); + } + + return persistence.ReadAsync(); + } + + private void ApplyState(T value) + { + Value = value; + } + + public Task ClearAsync() + { + Value = new T(); + + return persistence.DeleteAsync(); + } + + public Task WriteAsync() + { + return persistence.WriteSnapshotAsync(Value); + } + + public Task WriteEventAsync(Envelope envelope) + { + return persistence.WriteEventAsync(envelope); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs b/backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IActivationLimit.cs rename to backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs diff --git a/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs b/backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs rename to backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs diff --git a/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs rename to backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs diff --git a/src/Squidex.Infrastructure/Orleans/IDeactivater.cs b/backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IDeactivater.cs rename to backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs diff --git a/src/Squidex.Infrastructure/Orleans/IGrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/IGrainState.cs rename to backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs new file mode 100644 index 000000000..184529d66 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/ILockGrain.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Orleans +{ + public interface ILockGrain : IGrainWithStringKey + { + Task AcquireLockAsync(string key); + + Task ReleaseLockAsync(string releaseToken); + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs new file mode 100644 index 000000000..966d16cae --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public interface IUniqueNameIndexGrain + { + Task ReserveAsync(T id, string name); + + Task AddAsync(string? token); + + Task CountAsync(); + + Task RemoveReservationAsync(string? token); + + Task RemoveAsync(T id); + + Task RebuildAsync(Dictionary values); + + Task ClearAsync(); + + Task GetIdAsync(string name); + + Task> GetIdsAsync(string[] names); + + Task> GetIdsAsync(); + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs new file mode 100644 index 000000000..d30d9714d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class IdsIndexGrain : Grain, IIdsIndexGrain where TState : IdsIndexState, new() + { + private readonly IGrainState state; + + public IdsIndexGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task CountAsync() + { + return Task.FromResult(state.Value.Ids.Count); + } + + public Task RebuildAsync(HashSet ids) + { + state.Value = new TState { Ids = ids }; + + return state.WriteAsync(); + } + + public Task AddAsync(T id) + { + state.Value.Ids.Add(id); + + return state.WriteAsync(); + } + + public Task RemoveAsync(T id) + { + state.Value.Ids.Remove(id); + + return state.WriteAsync(); + } + + public Task ClearAsync() + { + return state.ClearAsync(); + } + + public Task> GetIdsAsync() + { + return Task.FromResult(state.Value.Ids.ToList()); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs new file mode 100644 index 000000000..ba2afdc3d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs @@ -0,0 +1,138 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexGrain : Grain, IUniqueNameIndexGrain where TState : UniqueNameIndexState, new() + { + private readonly Dictionary reservations = new Dictionary(); + private readonly IGrainState state; + + public UniqueNameIndexGrain(IGrainState state) + { + Guard.NotNull(state); + + this.state = state; + } + + public Task CountAsync() + { + return Task.FromResult(state.Value.Names.Count); + } + + public Task ClearAsync() + { + reservations.Clear(); + + return state.ClearAsync(); + } + + public Task RebuildAsync(Dictionary names) + { + state.Value = new TState { Names = names }; + + return state.WriteAsync(); + } + + public Task ReserveAsync(T id, string name) + { + string? token = null; + + if (!IsInUse(name) && !IsReserved(name)) + { + token = RandomHash.Simple(); + + reservations.Add(token, (name, id)); + } + + return Task.FromResult(token); + } + + public async Task AddAsync(string? token) + { + token ??= string.Empty; + + if (reservations.TryGetValue(token, out var reservation)) + { + state.Value.Names.Add(reservation.Name, reservation.Id); + + await state.WriteAsync(); + + reservations.Remove(token); + + return true; + } + + return false; + } + + public Task RemoveReservationAsync(string? token) + { + reservations.Remove(token ?? string.Empty); + + return TaskHelper.Done; + } + + public async Task RemoveAsync(T id) + { + var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key; + + if (name != null) + { + state.Value.Names.Remove(name); + + await state.WriteAsync(); + } + } + + public Task> GetIdsAsync(string[] names) + { + var result = new List(); + + if (names != null) + { + foreach (var name in names) + { + if (state.Value.Names.TryGetValue(name, out var id)) + { + result.Add(id); + } + } + } + + return Task.FromResult(result); + } + + public Task GetIdAsync(string name) + { + state.Value.Names.TryGetValue(name, out var id); + + return Task.FromResult(id); + } + + public Task> GetIdsAsync() + { + return Task.FromResult(state.Value.Names.Values.ToList()); + } + + private bool IsInUse(string name) + { + return state.Value.Names.ContainsKey(name); + } + + private bool IsReserved(string name) + { + return reservations.Values.Any(x => x.Name == name); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs diff --git a/src/Squidex.Infrastructure/Orleans/J.cs b/backend/src/Squidex.Infrastructure/Orleans/J.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/J.cs rename to backend/src/Squidex.Infrastructure/Orleans/J.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/J{T}.cs b/backend/src/Squidex.Infrastructure/Orleans/J{T}.cs new file mode 100644 index 000000000..8584b59b5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/J{T}.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Orleans.CodeGeneration; +using Orleans.Concurrency; +using Orleans.Serialization; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Infrastructure.Orleans +{ + [Immutable] + public struct J + { + public T Value { get; } + + public J(T value) + { + Value = value; + } + + public static implicit operator T(J value) + { + return value.Value; + } + + public static implicit operator J(T d) + { + return new J(d); + } + + public override string ToString() + { + return Value?.ToString() ?? string.Empty; + } + + public static Task> AsTask(T value) + { + return Task.FromResult>(value); + } + + [CopierMethod] + public static object? Copy(object? input, ICopyContext? context) + { + return input; + } + + [SerializerMethod] + public static void Serialize(object? input, ISerializationContext context, Type? expected) + { + using (Profiler.TraceMethod(nameof(J))) + { + var jsonSerializer = GetSerializer(context); + + var stream = new StreamWriterWrapper(context.StreamWriter); + + jsonSerializer.Serialize(input, stream); + } + } + + [DeserializerMethod] + public static object? Deserialize(Type expected, IDeserializationContext context) + { + using (Profiler.TraceMethod(nameof(J))) + { + var jsonSerializer = GetSerializer(context); + + var stream = new StreamReaderWrapper(context.StreamReader); + + return jsonSerializer.Deserialize(stream, expected); + } + } + + private static IJsonSerializer GetSerializer(ISerializerContext context) + { + try + { + return context?.ServiceProvider?.GetRequiredService() ?? J.DefaultSerializer; + } + catch + { + return J.DefaultSerializer; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs new file mode 100644 index 000000000..68700741c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LocalCacheFilter : IIncomingGrainCallFilter + { + private readonly ILocalCache localCache; + + public LocalCacheFilter(ILocalCache localCache) + { + Guard.NotNull(localCache); + + this.localCache = localCache; + } + + public async Task Invoke(IIncomingGrainCallContext context) + { + if (!context.Grain.GetType().Namespace!.StartsWith("Orleans", StringComparison.OrdinalIgnoreCase)) + { + using (localCache.StartContext()) + { + await context.Invoke(); + } + } + else + { + await context.Invoke(); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs new file mode 100644 index 000000000..5c9d3f5be --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/LockGrain.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LockGrain : GrainOfString, ILockGrain + { + private readonly Dictionary locks = new Dictionary(); + + public Task AcquireLockAsync(string key) + { + string? releaseToken = null; + + if (!locks.ContainsKey(key)) + { + releaseToken = Guid.NewGuid().ToString(); + + locks.Add(key, releaseToken); + } + + return Task.FromResult(releaseToken); + } + + public Task ReleaseLockAsync(string releaseToken) + { + var key = locks.FirstOrDefault(x => x.Value == releaseToken).Key; + + if (!string.IsNullOrWhiteSpace(key)) + { + locks.Remove(key); + } + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs new file mode 100644 index 000000000..61ff39d00 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LoggingFilter : IIncomingGrainCallFilter + { + private readonly ISemanticLog log; + + public LoggingFilter(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public async Task Invoke(IIncomingGrainCallContext context) + { + try + { + await context.Invoke(); + } + catch (DomainException) + { + throw; + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "GrainInvoked") + .WriteProperty("status", "Failed") + .WriteProperty("grain", context.Grain.ToString()) + .WriteProperty("grainMethod", context.ImplementationMethod.ToString())); + + throw; + } + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/SingleGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/SingleGrain.cs rename to backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs diff --git a/src/Squidex.Infrastructure/Orleans/StateFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/StateFilter.cs rename to backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs diff --git a/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs new file mode 100644 index 000000000..e2d2aec5b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Orleans.Serialization; + +namespace Squidex.Infrastructure.Orleans +{ + internal sealed class StreamReaderWrapper : Stream + { + private readonly IBinaryTokenStreamReader reader; + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get => throw new NotSupportedException(); + } + + public override long Position + { + get + { + return reader.CurrentPosition; + } + set + { + throw new NotSupportedException(); + } + } + + public StreamReaderWrapper(IBinaryTokenStreamReader reader) + { + this.reader = reader; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + var bytesLeft = (int)(reader.Length - reader.CurrentPosition); + + if (bytesLeft < count) + { + count = bytesLeft; + } + + reader.ReadByteArray(buffer, offset, count); + + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs similarity index 100% rename from src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs rename to backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs diff --git a/src/Squidex.Infrastructure/Plugins/IPlugin.cs b/backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs similarity index 100% rename from src/Squidex.Infrastructure/Plugins/IPlugin.cs rename to backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs diff --git a/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs b/backend/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs similarity index 100% rename from src/Squidex.Infrastructure/Plugins/IWebPlugin.cs rename to backend/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs diff --git a/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs new file mode 100644 index 000000000..dd14f3ca3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Plugins +{ + public sealed class PluginManager + { + private readonly HashSet loadedPlugins = new HashSet(); + private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); + + public IReadOnlyCollection Plugins + { + get { return loadedPlugins; } + } + + public void Add(string name, Assembly assembly) + { + Guard.NotNull(assembly); + + var pluginTypes = + assembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t)) + .Where(t => !t.IsAbstract); + + foreach (var pluginType in pluginTypes) + { + try + { + var plugin = (IPlugin)Activator.CreateInstance(pluginType)!; + + loadedPlugins.Add(plugin); + } + catch (Exception ex) + { + LogException(name, "Instantiating", ex); + } + } + } + + public void LogException(string plugin, string action, Exception exception) + { + Guard.NotNull(plugin); + Guard.NotNull(action); + Guard.NotNull(exception); + + exceptions.Add((plugin, action, exception)); + } + + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + Guard.NotNull(services); + Guard.NotNull(config); + + foreach (var plugin in loadedPlugins) + { + plugin.ConfigureServices(services, config); + } + } + + public void ConfigureBefore(IApplicationBuilder app) + { + Guard.NotNull(app); + + foreach (var plugin in loadedPlugins.OfType()) + { + plugin.ConfigureBefore(app); + } + } + + public void ConfigureAfter(IApplicationBuilder app) + { + Guard.NotNull(app); + + foreach (var plugin in loadedPlugins.OfType()) + { + plugin.ConfigureAfter(app); + } + } + + public void Log(ISemanticLog log) + { + Guard.NotNull(log); + + if (loadedPlugins.Count > 0 || exceptions.Count > 0) + { + var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; + + log.LogInformation(w => w + .WriteProperty("action", "pluginsLoaded") + .WriteProperty("status", status) + .WriteArray("errors", e => + { + foreach (var error in exceptions) + { + e.WriteObject(x => x + .WriteProperty("plugin", error.Plugin) + .WriteProperty("action", error.Action) + .WriteException(error.Exception)); + } + }) + .WriteArray("plugins", a => + { + foreach (var plugin in loadedPlugins) + { + a.WriteValue(plugin.GetType().ToString()); + } + })); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Plugins/PluginOptions.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs similarity index 100% rename from src/Squidex.Infrastructure/Plugins/PluginOptions.cs rename to backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs new file mode 100644 index 000000000..c784969bd --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public static class ClrFilter + { + public static LogicalFilter And(params FilterNode[] filters) + { + return new LogicalFilter(LogicalFilterType.And, filters); + } + + public static LogicalFilter Or(params FilterNode[] filters) + { + return new LogicalFilter(LogicalFilterType.Or, filters); + } + + public static NegateFilter Not(FilterNode filter) + { + return new NegateFilter(filter); + } + + public static CompareFilter Eq(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Equals, value); + } + + public static CompareFilter Ne(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.NotEquals, value); + } + + public static CompareFilter Lt(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.LessThan, value); + } + + public static CompareFilter Le(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.LessThanOrEqual, value); + } + + public static CompareFilter Gt(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.GreaterThan, value); + } + + public static CompareFilter Ge(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.GreaterThanOrEqual, value); + } + + public static CompareFilter Contains(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Contains, value); + } + + public static CompareFilter EndsWith(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.EndsWith, value); + } + + public static CompareFilter StartsWith(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.StartsWith, value); + } + + public static CompareFilter Empty(PropertyPath path) + { + return Binary(path, CompareOperator.Empty, null); + } + + public static CompareFilter In(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.In, value); + } + + private static CompareFilter Binary(PropertyPath path, CompareOperator @operator, ClrValue? value) + { + return new CompareFilter(path, @operator, value ?? ClrValue.Null); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/ClrQuery.cs b/backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/ClrQuery.cs rename to backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs new file mode 100644 index 000000000..8fe6ffdc0 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using NodaTime; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class ClrValue + { + public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); + + public object? Value { get; } + + public ClrValueType ValueType { get; } + + public bool IsList { get; } + + private ClrValue(object? value, ClrValueType valueType, bool isList) + { + Value = value; + ValueType = valueType; + + IsList = isList; + } + + public static implicit operator ClrValue(Instant value) + { + return new ClrValue(value, ClrValueType.Instant, false); + } + + public static implicit operator ClrValue(Guid value) + { + return new ClrValue(value, ClrValueType.Guid, false); + } + + public static implicit operator ClrValue(bool value) + { + return new ClrValue(value, ClrValueType.Boolean, false); + } + + public static implicit operator ClrValue(float value) + { + return new ClrValue(value, ClrValueType.Single, false); + } + + public static implicit operator ClrValue(double value) + { + return new ClrValue(value, ClrValueType.Double, false); + } + + public static implicit operator ClrValue(int value) + { + return new ClrValue(value, ClrValueType.Int32, false); + } + + public static implicit operator ClrValue(long value) + { + return new ClrValue(value, ClrValueType.Int64, false); + } + + public static implicit operator ClrValue(string? value) + { + return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; + } + + public static implicit operator ClrValue(List value) + { + return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; + } + + public override string ToString() + { + if (Value is IList list) + { + return $"[{string.Join(", ", list.OfType().Select(ToString).ToArray())}]"; + } + + return ToString(Value); + } + + private static string ToString(object? value) + { + if (value == null) + { + return "null"; + } + + if (value is string s) + { + return $"'{s.Replace("'", "\\'")}'"; + } + + return string.Format(CultureInfo.InvariantCulture, "{0}", value); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/ClrValueType.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/ClrValueType.cs rename to backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs new file mode 100644 index 000000000..3f0ba063d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public sealed class CompareFilter : FilterNode + { + public PropertyPath Path { get; } + + public CompareOperator Operator { get; } + + public TValue Value { get; } + + public CompareFilter(PropertyPath path, CompareOperator @operator, TValue value) + { + Guard.NotNull(path); + Guard.NotNull(value); + Guard.Enum(@operator); + + Path = path; + + Operator = @operator; + + Value = value; + } + + public override T Accept(FilterNodeVisitor visitor) + { + return visitor.Visit(this); + } + + public override string ToString() + { + switch (Operator) + { + case CompareOperator.Contains: + return $"contains({Path}, {Value})"; + case CompareOperator.Empty: + return $"empty({Path})"; + case CompareOperator.EndsWith: + return $"endsWith({Path}, {Value})"; + case CompareOperator.StartsWith: + return $"startsWith({Path}, {Value})"; + case CompareOperator.Equals: + return $"{Path} == {Value}"; + case CompareOperator.NotEquals: + return $"{Path} != {Value}"; + case CompareOperator.GreaterThan: + return $"{Path} > {Value}"; + case CompareOperator.GreaterThanOrEqual: + return $"{Path} >= {Value}"; + case CompareOperator.LessThan: + return $"{Path} < {Value}"; + case CompareOperator.LessThanOrEqual: + return $"{Path} <= {Value}"; + case CompareOperator.In: + return $"{Path} in {Value}"; + default: + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/CompareOperator.cs b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/CompareOperator.cs rename to backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs new file mode 100644 index 000000000..125787f11 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public abstract class FilterNode + { + public abstract T Accept(FilterNodeVisitor visitor); + + public abstract override string ToString(); + } +} diff --git a/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs new file mode 100644 index 000000000..6b7337f0e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure.Queries.Json +{ + public sealed class FilterConverter : JsonClassConverter> + { + public override IEnumerable SupportedTypes + { + get + { + yield return typeof(CompareFilter); + yield return typeof(FilterNode); + yield return typeof(LogicalFilter); + yield return typeof(NegateFilter); + } + } + + public override bool CanConvert(Type objectType) + { + return SupportedTypes.Contains(objectType); + } + + protected override FilterNode ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); + } + + FilterNode? result = null; + + PropertyPath? comparePath = null; + + var compareOperator = (CompareOperator)99; + + IJsonValue? compareValue = null; + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var propertyName = reader.Value.ToString()!; + + if (!reader.Read()) + { + throw new JsonSerializationException("Unexpected end when reading filter."); + } + + if (result != null) + { + throw new JsonSerializationException($"Unexpected property {propertyName}"); + } + + switch (propertyName.ToLowerInvariant()) + { + case "not": + var filter = serializer.Deserialize>(reader); + + result = new NegateFilter(filter); + break; + case "and": + var andFilters = serializer.Deserialize>>(reader); + + result = new LogicalFilter(LogicalFilterType.And, andFilters); + break; + case "or": + var orFilters = serializer.Deserialize>>(reader); + + result = new LogicalFilter(LogicalFilterType.Or, orFilters); + break; + case "path": + comparePath = serializer.Deserialize(reader); + break; + case "op": + compareOperator = ReadOperator(reader, serializer); + break; + case "value": + compareValue = serializer.Deserialize(reader); + break; + } + + break; + case JsonToken.Comment: + break; + case JsonToken.EndObject: + if (result != null) + { + return result; + } + + if (comparePath == null) + { + throw new JsonSerializationException("Path not defined."); + } + + if (compareValue == null && compareOperator != CompareOperator.Empty) + { + throw new JsonSerializationException("Value not defined."); + } + + if (!compareOperator.IsEnumValue()) + { + throw new JsonSerializationException("Operator not defined."); + } + + return new CompareFilter(comparePath, compareOperator, compareValue ?? JsonValue.Null); + } + } + + throw new JsonSerializationException("Unexpected end when reading filter."); + } + + private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + + switch (value.ToLowerInvariant()) + { + case "eq": + return CompareOperator.Equals; + case "ne": + return CompareOperator.NotEquals; + case "lt": + return CompareOperator.LessThan; + case "le": + return CompareOperator.LessThanOrEqual; + case "gt": + return CompareOperator.GreaterThan; + case "ge": + return CompareOperator.GreaterThanOrEqual; + case "empty": + return CompareOperator.Empty; + case "contains": + return CompareOperator.Contains; + case "endswith": + return CompareOperator.EndsWith; + case "startswith": + return CompareOperator.StartsWith; + case "in": + return CompareOperator.In; + } + + throw new JsonSerializationException($"Unexpected compare operator, got {value}."); + } + + protected override void WriteValue(JsonWriter writer, FilterNode value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs new file mode 100644 index 000000000..2e3192719 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.Queries.Json +{ + public sealed class JsonFilterVisitor : FilterNodeVisitor, IJsonValue> + { + private readonly List errors; + private readonly JsonSchema schema; + + private JsonFilterVisitor(JsonSchema schema, List errors) + { + this.schema = schema; + + this.errors = errors; + } + + public static FilterNode? Parse(FilterNode filter, JsonSchema schema, List errors) + { + var visitor = new JsonFilterVisitor(schema, errors); + + var parsed = filter.Accept(visitor); + + if (visitor.errors.Count > 0) + { + return null; + } + else + { + return parsed; + } + } + + public override FilterNode Visit(NegateFilter nodeIn) + { + return new NegateFilter(nodeIn.Accept(this)); + } + + public override FilterNode Visit(LogicalFilter nodeIn) + { + return new LogicalFilter(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); + } + + public override FilterNode Visit(CompareFilter nodeIn) + { + CompareFilter? result = null; + + if (nodeIn.Path.TryGetProperty(schema, errors, out var property)) + { + var isValidOperator = OperatorValidator.IsAllowedOperator(property, nodeIn.Operator); + + if (!isValidOperator) + { + errors.Add($"{nodeIn.Operator} is not a valid operator for type {property.Type} at {nodeIn.Path}."); + } + + var value = ValueConverter.Convert(property, nodeIn.Value, nodeIn.Path, errors); + + if (value != null && isValidOperator) + { + if (value.IsList && nodeIn.Operator != CompareOperator.In) + { + errors.Add($"Array value is not allowed for '{nodeIn.Operator}' operator and path '{nodeIn.Path}'."); + } + + result = new CompareFilter(nodeIn.Path, nodeIn.Operator, value); + } + } + + result ??= new CompareFilter(nodeIn.Path, nodeIn.Operator, ClrValue.Null); + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs rename to backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs diff --git a/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs rename to backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs new file mode 100644 index 000000000..bf3b1d113 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using NJsonSchema; + +namespace Squidex.Infrastructure.Queries.Json +{ + public static class PropertyPathValidator + { + public static bool TryGetProperty(this PropertyPath path, JsonSchema schema, List errors, [MaybeNullWhen(false)] out JsonSchema property) + { + foreach (var element in path) + { + var parent = schema.Reference ?? schema; + + if (parent.Properties.TryGetValue(element, out var p)) + { + schema = p; + } + else + { + if (!string.IsNullOrWhiteSpace(parent.Title)) + { + errors.Add($"'{element}' is not a property of '{parent.Title}'."); + } + else + { + errors.Add($"Path '{path}' does not point to a valid property in the model."); + } + + property = null!; + + return false; + } + } + + property = schema; + + return true; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs new file mode 100644 index 000000000..4813e90de --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NJsonSchema; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Infrastructure.Queries.Json +{ + public static class QueryParser + { + public static ClrQuery Parse(this JsonSchema schema, string json, IJsonSerializer jsonSerializer) + { + var query = ParseFromJson(json, jsonSerializer); + + var result = SimpleMapper.Map(query, new ClrQuery()); + + var errors = new List(); + + ConvertSorting(schema, result, errors); + ConvertFilters(schema, result, errors, query); + + if (errors.Count > 0) + { + throw new ValidationException("Failed to parse json query", errors.Select(x => new ValidationError(x)).ToArray()); + } + + return result; + } + + private static void ConvertFilters(JsonSchema schema, ClrQuery result, List errors, Query query) + { + if (query.Filter != null) + { + var filter = JsonFilterVisitor.Parse(query.Filter, schema, errors); + + if (filter != null) + { + result.Filter = Optimizer.Optimize(filter); + } + } + } + + private static void ConvertSorting(JsonSchema schema, ClrQuery result, List errors) + { + if (result.Sort != null) + { + foreach (var sorting in result.Sort) + { + sorting.Path.TryGetProperty(schema, errors, out _); + } + } + } + + private static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) + { + try + { + return jsonSerializer.Deserialize>(json); + } + catch (JsonException ex) + { + throw new ValidationException("Failed to parse json query.", new ValidationError(ex.Message)); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs new file mode 100644 index 000000000..a0318d14c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs @@ -0,0 +1,238 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NJsonSchema; +using NodaTime; +using NodaTime.Text; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.Queries.Json +{ + public static class ValueConverter + { + private delegate bool Parser(List errors, PropertyPath path, IJsonValue value, out T result); + + private static readonly InstantPattern[] InstantPatterns = + { + InstantPattern.General, + InstantPattern.ExtendedIso, + InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") + }; + + public static ClrValue? Convert(JsonSchema schema, IJsonValue value, PropertyPath path, List errors) + { + ClrValue? result = null; + + switch (GetType(schema)) + { + case JsonObjectType.Boolean: + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseBoolean); + } + else if (TryParseBoolean(errors, path, value, out var temp)) + { + result = temp; + } + + break; + } + + case JsonObjectType.Integer: + case JsonObjectType.Number: + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseNumber); + } + else if (TryParseNumber(errors, path, value, out var temp)) + { + result = temp; + } + + break; + } + + case JsonObjectType.String: + { + if (schema.Format == JsonFormatStrings.Guid) + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseGuid); + } + else if (TryParseGuid(errors, path, value, out var temp)) + { + result = temp; + } + } + else if (schema.Format == JsonFormatStrings.DateTime) + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseDateTime); + } + else if (TryParseDateTime(errors, path, value, out var temp)) + { + result = temp; + } + } + else + { + if (value is JsonArray jsonArray) + { + result = ParseArray(errors, path, jsonArray, TryParseString!); + } + else if (TryParseString(errors, path, value, out var temp)) + { + result = temp; + } + } + + break; + } + + default: + { + errors.Add($"Unsupported type {schema.Type} for {path}."); + break; + } + } + + return result; + } + + private static List ParseArray(List errors, PropertyPath path, JsonArray array, Parser parser) + { + var items = new List(); + + foreach (var item in array) + { + if (parser(errors, path, item, out var temp)) + { + items.Add(temp); + } + } + + return items; + } + + private static bool TryParseBoolean(List errors, PropertyPath path, IJsonValue value, out bool result) + { + result = default; + + if (value is JsonBoolean jsonBoolean) + { + result = jsonBoolean.Value; + + return true; + } + + errors.Add($"Expected Boolean for path '{path}', but got {value.Type}."); + + return false; + } + + private static bool TryParseNumber(List errors, PropertyPath path, IJsonValue value, out double result) + { + result = default; + + if (value is JsonNumber jsonNumber) + { + result = jsonNumber.Value; + + return true; + } + + errors.Add($"Expected Number for path '{path}', but got {value.Type}."); + + return false; + } + + private static bool TryParseString(List errors, PropertyPath path, IJsonValue value, out string? result) + { + result = default; + + if (value is JsonString jsonString) + { + result = jsonString.Value; + + return true; + } + else if (value is JsonNull) + { + return true; + } + + errors.Add($"Expected String for path '{path}', but got {value.Type}."); + + return false; + } + + private static bool TryParseGuid(List errors, PropertyPath path, IJsonValue value, out Guid result) + { + result = default; + + if (value is JsonString jsonString) + { + if (Guid.TryParse(jsonString.Value, out result)) + { + return true; + } + + errors.Add($"Expected Guid String for path '{path}', but got invalid String."); + } + else + { + errors.Add($"Expected Guid String for path '{path}', but got {value.Type}."); + } + + return false; + } + + private static bool TryParseDateTime(List errors, PropertyPath path, IJsonValue value, out Instant result) + { + result = default; + + if (value is JsonString jsonString) + { + foreach (var pattern in InstantPatterns) + { + var parsed = pattern.Parse(jsonString.Value); + + if (parsed.Success) + { + result = parsed.Value; + + return true; + } + } + + errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got invalid String."); + } + else + { + errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got {value.Type}."); + } + + return false; + } + + private static JsonObjectType GetType(JsonSchema schema) + { + if (schema.Item != null) + { + return schema.Item.Type; + } + + return schema.Type; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs b/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs new file mode 100644 index 000000000..9b207e76e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class LogicalFilter : FilterNode + { + public IReadOnlyList> Filters { get; } + + public LogicalFilterType Type { get; } + + public LogicalFilter(LogicalFilterType type, IReadOnlyList> filters) + { + Guard.NotNull(filters); + Guard.Enum(type); + + Filters = filters; + + Type = type; + } + + public override T Accept(FilterNodeVisitor visitor) + { + return visitor.Visit(this); + } + + public override string ToString() + { + return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs b/backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/LogicalFilterType.cs rename to backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs b/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs new file mode 100644 index 000000000..e4deb18d2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public sealed class NegateFilter : FilterNode + { + public FilterNode Filter { get; } + + public NegateFilter(FilterNode filter) + { + Guard.NotNull(filter); + + Filter = filter; + } + + public override T Accept(FilterNodeVisitor visitor) + { + return visitor.Visit(this); + } + + public override string ToString() + { + return $"!({Filter})"; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs new file mode 100644 index 000000000..54a6d5816 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs @@ -0,0 +1,178 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using NodaTime; +using NodaTime.Text; + +namespace Squidex.Infrastructure.Queries.OData +{ + public sealed class ConstantWithTypeVisitor : QueryNodeVisitor + { + private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); + private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); + private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); + private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); + private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); + private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); + private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); + + private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); + + private ConstantWithTypeVisitor() + { + } + + public static ClrValue Visit(QueryNode node) + { + return node.Accept(Instance); + } + + public override ClrValue Visit(ConvertNode nodeIn) + { + if (nodeIn.TypeReference.Definition == BooleanType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + return bool.Parse(value.ToString()!); + } + + if (nodeIn.TypeReference.Definition == GuidType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + return Guid.Parse(value.ToString()!); + } + + if (nodeIn.TypeReference.Definition == DateTimeType) + { + var value = ConstantVisitor.Visit(nodeIn.Source); + + return ParseInstant(value); + } + + if (ConstantVisitor.Visit(nodeIn.Source) == null) + { + return ClrValue.Null; + } + + throw new NotSupportedException(); + } + + public override ClrValue Visit(CollectionConstantNode nodeIn) + { + if (nodeIn.ItemType.Definition == DateTimeType) + { + return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList(); + } + + if (nodeIn.ItemType.Definition == GuidType) + { + return nodeIn.Collection.Select(x => (Guid)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == BooleanType) + { + return nodeIn.Collection.Select(x => (bool)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == SingleType) + { + return nodeIn.Collection.Select(x => (float)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == DoubleType) + { + return nodeIn.Collection.Select(x => (double)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == Int32Type) + { + return nodeIn.Collection.Select(x => (int)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == Int64Type) + { + return nodeIn.Collection.Select(x => (long)x.Value).ToList(); + } + + if (nodeIn.ItemType.Definition == StringType) + { + return nodeIn.Collection.Select(x => (string)x.Value).ToList(); + } + + throw new NotSupportedException(); + } + + public override ClrValue Visit(ConstantNode nodeIn) + { + if (nodeIn.TypeReference.Definition == BooleanType) + { + return (bool)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == SingleType) + { + return (float)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == DoubleType) + { + return (double)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == Int32Type) + { + return (int)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == Int64Type) + { + return (long)nodeIn.Value; + } + + if (nodeIn.TypeReference.Definition == StringType) + { + return (string)nodeIn.Value; + } + + throw new NotSupportedException(); + } + + private static Instant ParseInstant(object value) + { + if (value is DateTimeOffset dateTimeOffset) + { + return Instant.FromDateTimeOffset(dateTimeOffset.Add(dateTimeOffset.Offset)); + } + + if (value is DateTime dateTime) + { + return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); + } + + if (value is Date date) + { + return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); + } + + var parseResult = InstantPattern.General.Parse(value.ToString()); + + if (!parseResult.Success) + { + throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); + } + + return parseResult.Value; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs new file mode 100644 index 000000000..bf25ddb59 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Squidex.Infrastructure.Queries.OData +{ + public static class EdmModelExtensions + { + static EdmModelExtensions() + { + CustomUriFunctions.AddCustomUriFunction("empty", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetString(true))); + } + + public static ODataUriParser? ParseQuery(this IEdmModel model, string query) + { + if (!model.EntityContainer.EntitySets().Any()) + { + return null; + } + + query ??= string.Empty; + + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); + + if (query.StartsWith("?", StringComparison.Ordinal)) + { + query = query.Substring(1); + } + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + + public static ClrQuery ToQuery(this ODataUriParser? parser) + { + var query = new ClrQuery(); + + if (parser != null) + { + parser.ParseTake(query); + parser.ParseSkip(query); + parser.ParseFilter(query); + parser.ParseSort(query); + } + + return query; + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs diff --git a/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs rename to backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs b/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs new file mode 100644 index 000000000..1514135a4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class Optimizer : TransformVisitor + { + private static readonly Optimizer Instance = new Optimizer(); + + private Optimizer() + { + } + + public static FilterNode? Optimize(FilterNode source) + { + return source?.Accept(Instance); + } + + public override FilterNode? Visit(LogicalFilter nodeIn) + { + var pruned = nodeIn.Filters.Select(x => x.Accept(this)!).Where(x => x != null).ToList(); + + if (pruned.Count == 1) + { + return pruned[0]; + } + + if (pruned.Count == 0) + { + return null; + } + + return new LogicalFilter(nodeIn.Type, pruned); + } + + public override FilterNode? Visit(NegateFilter nodeIn) + { + var pruned = nodeIn.Filter.Accept(this); + + if (pruned == null) + { + return null; + } + + if (pruned is CompareFilter comparison) + { + if (comparison.Operator == CompareOperator.Equals) + { + return new CompareFilter(comparison.Path, CompareOperator.NotEquals, comparison.Value); + } + + if (comparison.Operator == CompareOperator.NotEquals) + { + return new CompareFilter(comparison.Path, CompareOperator.Equals, comparison.Value); + } + } + + return new NegateFilter(pruned); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs b/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs new file mode 100644 index 000000000..4bbb67ff1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class PascalCasePathConverter : TransformVisitor + { + private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter(); + + private PascalCasePathConverter() + { + } + + public static FilterNode? Transform(FilterNode node) + { + return node.Accept(Instance); + } + + public override FilterNode? Visit(CompareFilter nodeIn) + { + return new CompareFilter(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs b/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs new file mode 100644 index 000000000..a6bf83e07 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class PropertyPath : ReadOnlyCollection + { + private static readonly char[] Separators = { '.', '/' }; + + public PropertyPath(IList items) + : base(items) + { + if (items.Count == 0) + { + throw new ArgumentException("Path cannot be empty.", nameof(items)); + } + } + + public static implicit operator PropertyPath(string path) + { + return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries).ToList()!); + } + + public static implicit operator PropertyPath(string[] path) + { + return new PropertyPath(path?.ToList()!); + } + + public static implicit operator PropertyPath(List path) + { + return new PropertyPath(path); + } + + public static implicit operator PropertyPath(ImmutableList path) + { + return new PropertyPath(path); + } + + public override string ToString() + { + return string.Join(".", this); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Queries/Query.cs b/backend/src/Squidex.Infrastructure/Queries/Query.cs new file mode 100644 index 000000000..25ab0b60a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/Query.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Queries +{ + public class Query + { + public FilterNode? Filter { get; set; } + + public string? FullText { get; set; } + + public long Skip { get; set; } + + public long Take { get; set; } = long.MaxValue; + + public List Sort { get; set; } = new List(); + + public override string ToString() + { + var parts = new List(); + + if (Filter != null) + { + parts.Add($"Filter: {Filter}"); + } + + if (FullText != null) + { + parts.Add($"FullText: '{FullText.Replace("'", "\'")}'"); + } + + if (Skip > 0) + { + parts.Add($"Skip: {Skip}"); + } + + if (Take < long.MaxValue) + { + parts.Add($"Take: {Take}"); + } + + if (Sort.Count > 0) + { + parts.Add($"Sort: {string.Join(", ", Sort)}"); + } + + return string.Join("; ", parts); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/SortBuilder.cs rename to backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/SortNode.cs b/backend/src/Squidex.Infrastructure/Queries/SortNode.cs new file mode 100644 index 000000000..174bb7c22 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/SortNode.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Queries +{ + public sealed class SortNode + { + public PropertyPath Path { get; } + + public SortOrder Order { get; set; } + + public SortNode(PropertyPath path, SortOrder order) + { + Guard.NotNull(path); + Guard.Enum(order); + + Path = path; + + Order = order; + } + + public override string ToString() + { + var path = string.Join(".", Path); + + return $"{path} {Order}"; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/SortOrder.cs b/backend/src/Squidex.Infrastructure/Queries/SortOrder.cs similarity index 100% rename from src/Squidex.Infrastructure/Queries/SortOrder.cs rename to backend/src/Squidex.Infrastructure/Queries/SortOrder.cs diff --git a/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs new file mode 100644 index 000000000..8d6f9b311 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; + +namespace Squidex.Infrastructure.Queries +{ + public abstract class TransformVisitor : FilterNodeVisitor?, TValue> + { + public override FilterNode? Visit(CompareFilter nodeIn) + { + return nodeIn; + } + + public override FilterNode? Visit(LogicalFilter nodeIn) + { + var inner = nodeIn.Filters.Select(x => x.Accept(this)!).Where(x => x != null).ToList(); + + return new LogicalFilter(nodeIn.Type, inner); + } + + public override FilterNode? Visit(NegateFilter nodeIn) + { + var inner = nodeIn.Filter.Accept(this); + + if (inner == null) + { + return inner; + } + + return new NegateFilter(inner); + } + } +} diff --git a/src/Squidex.Infrastructure/RandomHash.cs b/backend/src/Squidex.Infrastructure/RandomHash.cs similarity index 100% rename from src/Squidex.Infrastructure/RandomHash.cs rename to backend/src/Squidex.Infrastructure/RandomHash.cs diff --git a/backend/src/Squidex.Infrastructure/RefToken.cs b/backend/src/Squidex.Infrastructure/RefToken.cs new file mode 100644 index 000000000..1316019ae --- /dev/null +++ b/backend/src/Squidex.Infrastructure/RefToken.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Squidex.Infrastructure +{ + public sealed class RefToken : IEquatable + { + public string Type { get; } + + public string Identifier { get; } + + public bool IsClient + { + get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } + } + + public bool IsSubject + { + get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } + } + + public RefToken(string type, string identifier) + { + Guard.NotNullOrEmpty(type); + Guard.NotNullOrEmpty(identifier); + + Type = type.ToLowerInvariant(); + + Identifier = identifier; + } + + public override string ToString() + { + return $"{Type}:{Identifier}"; + } + + public override bool Equals(object? obj) + { + return Equals(obj as RefToken); + } + + public bool Equals(RefToken? other) + { + return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier))); + } + + public override int GetHashCode() + { + return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode(); + } + + public static bool TryParse(string value, [MaybeNullWhen(false)] out RefToken result) + { + if (value != null) + { + var idx = value.IndexOf(':'); + + if (idx > 0 && idx < value.Length - 1) + { + result = new RefToken(value.Substring(0, idx), value.Substring(idx + 1)); + + return true; + } + } + + result = null!; + + return false; + } + + public static RefToken Parse(string value) + { + if (!TryParse(value, out var result)) + { + throw new ArgumentException("Ref token must have more than 2 parts divided by colon.", nameof(value)); + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/RefTokenType.cs b/backend/src/Squidex.Infrastructure/RefTokenType.cs similarity index 100% rename from src/Squidex.Infrastructure/RefTokenType.cs rename to backend/src/Squidex.Infrastructure/RefTokenType.cs diff --git a/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs b/backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs rename to backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs new file mode 100644 index 000000000..314921b2c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Reflection +{ + public interface IPropertyAccessor + { + object? Get(object target); + + void Set(object target, object? value); + } +} diff --git a/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs b/backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/ITypeProvider.cs rename to backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs new file mode 100644 index 000000000..9d3e2756b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; + +namespace Squidex.Infrastructure.Reflection +{ + public sealed class PropertiesTypeAccessor + { + private static readonly ConcurrentDictionary AccessorCache = new ConcurrentDictionary(); + private readonly Dictionary accessors = new Dictionary(); + private readonly List properties = new List(); + + public IEnumerable Properties + { + get + { + return properties; + } + } + + private PropertiesTypeAccessor(Type type) + { + var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in allProperties) + { + accessors[property.Name] = new PropertyAccessor(type, property); + + properties.Add(property); + } + } + + public static PropertiesTypeAccessor Create(Type targetType) + { + Guard.NotNull(targetType); + + return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x)); + } + + public void SetValue(object target, string propertyName, object? value) + { + Guard.NotNull(target); + + var accessor = FindAccessor(propertyName); + + accessor.Set(target, value); + } + + public object? GetValue(object target, string propertyName) + { + Guard.NotNull(target); + + var accessor = FindAccessor(propertyName); + + return accessor.Get(target); + } + + private IPropertyAccessor FindAccessor(string propertyName) + { + Guard.NotNullOrEmpty(propertyName); + + if (!accessors.TryGetValue(propertyName, out var accessor)) + { + throw new ArgumentException("Property does not exist.", nameof(propertyName)); + } + + return accessor; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs new file mode 100644 index 000000000..e73ce8816 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.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.Reflection; + +namespace Squidex.Infrastructure.Reflection +{ + public sealed class PropertyAccessor : IPropertyAccessor + { + private sealed class PropertyWrapper : IPropertyAccessor + { + private readonly Func getMethod; + private readonly Action setMethod; + + public PropertyWrapper(PropertyInfo propertyInfo) + { + if (propertyInfo.CanRead) + { + getMethod = (Func)propertyInfo.GetGetMethod(true)!.CreateDelegate(typeof(Func)); + } + else + { + getMethod = x => throw new NotSupportedException(); + } + + if (propertyInfo.CanWrite) + { + setMethod = (Action)propertyInfo.GetSetMethod(true)!.CreateDelegate(typeof(Action)); + } + else + { + setMethod = (x, y) => throw new NotSupportedException(); + } + } + + public object? Get(object source) + { + return getMethod((TObject)source); + } + + public void Set(object source, object? value) + { + setMethod((TObject)source, (TValue)value!); + } + } + + private readonly IPropertyAccessor internalAccessor; + + public PropertyAccessor(Type targetType, PropertyInfo propertyInfo) + { + Guard.NotNull(targetType); + Guard.NotNull(propertyInfo); + + internalAccessor = (IPropertyAccessor)Activator.CreateInstance(typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType), propertyInfo)!; + } + + public object? Get(object target) + { + Guard.NotNull(target); + + return internalAccessor.Get(target); + } + + public void Set(object target, object? value) + { + Guard.NotNull(target); + + internalAccessor.Set(target, value); + } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs b/backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs rename to backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs new file mode 100644 index 000000000..4bb7acccb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.Reflection +{ + public static class SimpleCopier + { + private struct PropertyMapper + { + private readonly IPropertyAccessor accessor; + private readonly Func converter; + + public PropertyMapper(IPropertyAccessor accessor, Func converter) + { + this.accessor = accessor; + this.converter = converter; + } + + public void MapProperty(object source, object target) + { + var value = converter(accessor.Get(source)); + + accessor.Set(target, value); + } + } + + private static class ClassCopier where T : class, new() + { + private static readonly List Mappers = new List(); + + static ClassCopier() + { + var type = typeof(T); + + foreach (var property in type.GetPublicProperties()) + { + if (!property.CanWrite || !property.CanRead) + { + continue; + } + + var accessor = new PropertyAccessor(type, property); + + if (property.PropertyType.Implements()) + { + Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x!)?.Clone())); + } + else + { + Mappers.Add(new PropertyMapper(accessor, x => x)); + } + } + } + + public static T CopyThis(T source) + { + var destination = new T(); + + foreach (var mapper in Mappers) + { + mapper.MapProperty(source, destination); + } + + return destination; + } + } + + public static T Copy(this T source) where T : class, new() + { + Guard.NotNull(source); + + return ClassCopier.CopyThis(source); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs new file mode 100644 index 000000000..fd2d4d76e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs @@ -0,0 +1,186 @@ +// ========================================================================== +// 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.Globalization; +using System.Linq; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.Reflection +{ + public static class SimpleMapper + { + private sealed class StringConversionPropertyMapper : PropertyMapper + { + public StringConversionPropertyMapper( + IPropertyAccessor sourceAccessor, + IPropertyAccessor targetAccessor) + : base(sourceAccessor, targetAccessor) + { + } + + public override void MapProperty(object source, object target, CultureInfo culture) + { + var value = GetValue(source); + + SetValue(target, value?.ToString()); + } + } + + private sealed class ConversionPropertyMapper : PropertyMapper + { + private readonly Type targetType; + + public ConversionPropertyMapper( + IPropertyAccessor sourceAccessor, + IPropertyAccessor targetAccessor, + Type targetType) + : base(sourceAccessor, targetAccessor) + { + this.targetType = targetType; + } + + public override void MapProperty(object source, object target, CultureInfo culture) + { + var value = GetValue(source); + + if (value == null) + { + return; + } + + try + { + var converted = Convert.ChangeType(value, targetType, culture); + + SetValue(target, converted); + } + catch + { + return; + } + } + } + + private class PropertyMapper + { + private readonly IPropertyAccessor sourceAccessor; + private readonly IPropertyAccessor targetAccessor; + + public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) + { + this.sourceAccessor = sourceAccessor; + this.targetAccessor = targetAccessor; + } + + public virtual void MapProperty(object source, object target, CultureInfo culture) + { + var value = GetValue(source); + + SetValue(target, value); + } + + protected void SetValue(object destination, object? value) + { + targetAccessor.Set(destination, value); + } + + protected object? GetValue(object source) + { + return sourceAccessor.Get(source); + } + } + + private static class ClassMapper where TSource : class where TTarget : class + { + private static readonly List Mappers = new List(); + + static ClassMapper() + { + var sourceClassType = typeof(TSource); + var sourceProperties = + sourceClassType.GetPublicProperties() + .Where(x => x.CanRead).ToList(); + + var targetClassType = typeof(TTarget); + var targetProperties = + targetClassType.GetPublicProperties() + .Where(x => x.CanWrite).ToList(); + + foreach (var sourceProperty in sourceProperties) + { + var targetProperty = targetProperties.FirstOrDefault(x => x.Name == sourceProperty.Name); + + if (targetProperty == null) + { + continue; + } + + var sourceType = sourceProperty.PropertyType; + var targetType = targetProperty.PropertyType; + + if (sourceType == targetType) + { + Mappers.Add(new PropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty))); + } + else if (targetType == typeof(string)) + { + Mappers.Add(new StringConversionPropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty))); + } + else if (sourceType.Implements() || targetType.Implements()) + { + Mappers.Add(new ConversionPropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty), + targetType)); + } + } + } + + public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) + { + foreach (var mapper in Mappers) + { + mapper.MapProperty(source, destination, culture); + } + + return destination; + } + } + + public static TTarget Map(TSource source) + where TSource : class + where TTarget : class, new() + { + return Map(source, new TTarget(), CultureInfo.CurrentCulture); + } + + public static TTarget Map(TSource source, TTarget target) + where TSource : class + where TTarget : class + { + return Map(source, target, CultureInfo.CurrentCulture); + } + + public static TTarget Map(TSource source, TTarget target, CultureInfo culture) + where TSource : class + where TTarget : class + { + Guard.NotNull(source); + Guard.NotNull(culture); + Guard.NotNull(target); + + return ClassMapper.MapClass(source, target, culture); + } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs rename to backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs rename to backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs similarity index 100% rename from src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs rename to backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs new file mode 100644 index 000000000..65d4570b9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs @@ -0,0 +1,163 @@ +// ========================================================================== +// 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.Reflection; + +namespace Squidex.Infrastructure.Reflection +{ + public sealed class TypeNameRegistry + { + private readonly Dictionary namesByType = new Dictionary(); + private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public TypeNameRegistry(IEnumerable? providers = null) + { + if (providers != null) + { + foreach (var provider in providers) + { + Map(provider); + } + } + } + + public TypeNameRegistry MapObsolete(Type type, string name) + { + Guard.NotNull(type); + Guard.NotNull(name); + + lock (namesByType) + { + if (typesByName.TryGetValue(name, out var existingType) && existingType != type) + { + var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; + + throw new ArgumentException(message, nameof(type)); + } + + typesByName[name] = type; + } + + return this; + } + + public TypeNameRegistry Map(ITypeProvider provider) + { + Guard.NotNull(provider); + + provider.Map(this); + + return this; + } + + public TypeNameRegistry Map(Type type) + { + Guard.NotNull(type); + + var typeNameAttribute = type.GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + { + Map(type, typeNameAttribute.TypeName); + } + + return this; + } + + public TypeNameRegistry Map(Type type, string name) + { + Guard.NotNull(type); + Guard.NotNull(name); + + lock (namesByType) + { + if (namesByType.TryGetValue(type, out var existingName) && existingName != name) + { + var message = $"The type '{type}' is already registered with name '{namesByType[type]}'"; + + throw new ArgumentException(message, nameof(type)); + } + + namesByType[type] = name; + + if (typesByName.TryGetValue(name, out var existingType) && existingType != type) + { + var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; + + throw new ArgumentException(message, nameof(type)); + } + + typesByName[name] = type; + } + + return this; + } + + public TypeNameRegistry MapUnmapped(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + if (!namesByType.ContainsKey(type)) + { + Map(type); + } + } + + return this; + } + + public string GetName() + { + return GetName(typeof(T)); + } + + public string GetNameOrNull() + { + return GetNameOrNull(typeof(T)); + } + + public string GetNameOrNull(Type type) + { + var result = namesByType.GetOrDefault(type); + + return result; + } + + public Type GetTypeOrNull(string name) + { + var result = typesByName.GetOrDefault(name); + + return result; + } + + public string GetName(Type type) + { + var result = namesByType.GetOrDefault(type); + + if (result == null) + { + throw new TypeNameNotFoundException($"There is no name for type '{type}"); + } + + return result; + } + + public Type GetType(string name) + { + var result = typesByName.GetOrDefault(name); + + if (result == null) + { + throw new TypeNameNotFoundException($"There is no type for name '{name}"); + } + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/ResultList.cs b/backend/src/Squidex.Infrastructure/ResultList.cs similarity index 100% rename from src/Squidex.Infrastructure/ResultList.cs rename to backend/src/Squidex.Infrastructure/ResultList.cs diff --git a/backend/src/Squidex.Infrastructure/RetryWindow.cs b/backend/src/Squidex.Infrastructure/RetryWindow.cs new file mode 100644 index 000000000..4f3250619 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/RetryWindow.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; + +namespace Squidex.Infrastructure +{ + public sealed class RetryWindow + { + private readonly Duration windowDuration; + private readonly int windowSize; + private readonly Queue retries = new Queue(); + private readonly IClock clock; + + public RetryWindow(TimeSpan windowDuration, int windowSize, IClock? clock = null) + { + this.windowDuration = Duration.FromTimeSpan(windowDuration); + this.windowSize = windowSize + 1; + + this.clock = clock ?? SystemClock.Instance; + } + + public void Reset() + { + retries.Clear(); + } + + public bool CanRetryAfterFailure() + { + var now = clock.GetCurrentInstant(); + + retries.Enqueue(now); + + while (retries.Count > windowSize) + { + retries.Dequeue(); + } + + return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Security/Extensions.cs b/backend/src/Squidex.Infrastructure/Security/Extensions.cs new file mode 100644 index 000000000..bc06e33a1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/Extensions.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Security.Claims; + +namespace Squidex.Infrastructure.Security +{ + public static class Extensions + { + public static RefToken? Token(this ClaimsPrincipal principal) + { + var subjectId = principal.OpenIdSubject(); + + if (!string.IsNullOrWhiteSpace(subjectId)) + { + return new RefToken(RefTokenType.Subject, subjectId); + } + + var clientId = principal.OpenIdClientId(); + + if (!string.IsNullOrWhiteSpace(clientId)) + { + return new RefToken(RefTokenType.Client, clientId); + } + + return null; + } + + public static string? OpenIdSubject(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Subject)?.Value; + } + + public static string? OpenIdClientId(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; + } + + public static string? UserOrClientId(this ClaimsPrincipal principal) + { + return principal.OpenIdSubject() ?? principal.OpenIdClientId(); + } + + public static string? OpenIdPreferredUserName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; + } + + public static string? OpenIdName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Name)?.Value; + } + + public static string? OpenIdNickName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.NickName)?.Value; + } + + public static string? OpenIdEmail(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; + } + + public static bool IsInClient(this ClaimsPrincipal principal, string client) + { + return principal.Claims.Any(x => x.Type == OpenIdClaims.ClientId && string.Equals(x.Value, client, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/Squidex.Infrastructure/Security/OpenIdClaims.cs b/backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs similarity index 100% rename from src/Squidex.Infrastructure/Security/OpenIdClaims.cs rename to backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs new file mode 100644 index 000000000..eb63501b3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; + +namespace Squidex.Infrastructure.Security +{ + public sealed partial class Permission + { + internal struct Part + { + private static readonly char[] AlternativeSeparators = { '|' }; + private static readonly char[] MainSeparators = { '.' }; + + public readonly string[]? Alternatives; + + public readonly bool Exclusion; + + public Part(string[]? alternatives, bool exclusion) + { + Alternatives = alternatives; + + Exclusion = exclusion; + } + + public static Part[] ParsePath(string path) + { + var parts = path.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries); + + var result = new Part[parts.Length]; + + for (var i = 0; i < result.Length; i++) + { + result[i] = Parse(parts[i]); + } + + return result; + } + + public static Part Parse(string part) + { + var isExclusion = false; + + if (part.StartsWith(Exclude, StringComparison.OrdinalIgnoreCase)) + { + isExclusion = true; + + part = part.Substring(1); + } + + string[]? alternatives = null; + + if (part != Any) + { + alternatives = part.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + return new Part(alternatives, isExclusion); + } + + public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) + { + if (lhs.Alternatives == null) + { + return true; + } + + if (allowNull && rhs.Alternatives == null) + { + return true; + } + + var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); + + return rhs.Alternatives != null && lhs.Alternatives.Intersect(rhs.Alternatives).Any() == shouldIntersect; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.cs b/backend/src/Squidex.Infrastructure/Security/Permission.cs new file mode 100644 index 000000000..c24849a24 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/Permission.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Security +{ + public sealed partial class Permission : IComparable, IEquatable + { + public const string Any = "*"; + public const string Exclude = "^"; + + private readonly string id; + private Part[] path; + + public string Id + { + get { return id; } + } + + private Part[] Path + { + get { return path ?? (path = Part.ParsePath(id)); } + } + + public Permission(string id) + { + Guard.NotNullOrEmpty(id); + + this.id = id; + } + + public bool Allows(Permission permission) + { + if (permission == null) + { + return false; + } + + return Covers(Path, permission.Path); + } + + public bool Includes(Permission permission) + { + if (permission == null) + { + return false; + } + + return PartialCovers(Path, permission.Path); + } + + private static bool Covers(Part[] given, Part[] requested) + { + if (given.Length > requested.Length) + { + return false; + } + + for (var i = 0; i < given.Length; i++) + { + if (!Part.Intersects(ref given[i], ref requested[i], false)) + { + return false; + } + } + + return true; + } + + private static bool PartialCovers(Part[] given, Part[] requested) + { + for (var i = 0; i < Math.Min(given.Length, requested.Length); i++) + { + if (!Part.Intersects(ref given[i], ref requested[i], true)) + { + return false; + } + } + + return true; + } + + public bool StartsWith(string test) + { + return id.StartsWith(test, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Permission); + } + + public bool Equals(Permission? other) + { + return other != null && string.Equals(id, other.id, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return id.GetHashCode(); + } + + public override string ToString() + { + return id; + } + + public int CompareTo(Permission other) + { + return other == null ? -1 : string.Compare(id, other.id, StringComparison.Ordinal); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs new file mode 100644 index 000000000..ee3c06055 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Infrastructure.Security +{ + public sealed class PermissionSet : IReadOnlyCollection + { + public static readonly PermissionSet Empty = new PermissionSet(Array.Empty()); + + private readonly List permissions; + private readonly Lazy display; + + public int Count + { + get { return permissions.Count; } + } + + public PermissionSet(params Permission[] permissions) + : this((IEnumerable)permissions) + { + } + + public PermissionSet(params string[] permissions) + : this(permissions?.Select(x => new Permission(x))!) + { + } + + public PermissionSet(IEnumerable permissions) + : this(permissions?.Select(x => new Permission(x))!) + { + } + + public PermissionSet(IEnumerable permissions) + { + Guard.NotNull(permissions); + + this.permissions = permissions.ToList(); + + display = new Lazy(() => string.Join(";", this.permissions)); + } + + public bool Allows(Permission? other) + { + if (other == null) + { + return false; + } + + return permissions.Any(x => x.Allows(other)); + } + + public bool Includes(Permission? other) + { + if (other == null) + { + return false; + } + + return permissions.Any(x => x.Includes(other)); + } + + public override string ToString() + { + return display.Value; + } + + public IEnumerable ToIds() + { + return permissions.Select(x => x.Id); + } + + public IEnumerator GetEnumerator() + { + return permissions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return permissions.GetEnumerator(); + } + } +} diff --git a/src/Squidex.Infrastructure/Singletons.cs b/backend/src/Squidex.Infrastructure/Singletons.cs similarity index 100% rename from src/Squidex.Infrastructure/Singletons.cs rename to backend/src/Squidex.Infrastructure/Singletons.cs diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj new file mode 100644 index 000000000..7248a0b61 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -0,0 +1,46 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs similarity index 100% rename from src/Squidex.Infrastructure/SquidexInfrastructure.cs rename to backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs diff --git a/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs b/backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/States/CollectionNameAttribute.cs rename to backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs diff --git a/backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs new file mode 100644 index 000000000..203dc924e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs @@ -0,0 +1,45 @@ + // ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.States +{ + public sealed class DefaultStreamNameResolver : IStreamNameResolver + { + private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; + + public string GetStreamName(Type aggregateType, string id) + { + Guard.NotNullOrEmpty(id); + Guard.NotNull(aggregateType); + + return $"{aggregateType.TypeName(true, Suffixes)}-{id}"; + } + + public string WithNewId(string streamName, Func idGenerator) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(idGenerator); + + var positionOfDash = streamName.IndexOf('-'); + + if (positionOfDash >= 0) + { + var newId = idGenerator(streamName.Substring(positionOfDash + 1)); + + if (!string.IsNullOrWhiteSpace(newId)) + { + streamName = $"{streamName.Substring(0, positionOfDash)}-{newId}"; + } + } + + return streamName; + } + } +} diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/backend/src/Squidex.Infrastructure/States/IPersistence.cs similarity index 100% rename from src/Squidex.Infrastructure/States/IPersistence.cs rename to backend/src/Squidex.Infrastructure/States/IPersistence.cs diff --git a/src/Squidex.Infrastructure/States/IPersistence{TState}.cs b/backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs similarity index 100% rename from src/Squidex.Infrastructure/States/IPersistence{TState}.cs rename to backend/src/Squidex.Infrastructure/States/IPersistence{TState}.cs diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs similarity index 100% rename from src/Squidex.Infrastructure/States/ISnapshotStore.cs rename to backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs diff --git a/backend/src/Squidex.Infrastructure/States/IStore.cs b/backend/src/Squidex.Infrastructure/States/IStore.cs new file mode 100644 index 000000000..892770065 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/IStore.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + public delegate void HandleEvent(Envelope @event); + + public delegate void HandleSnapshot(T state); + + public interface IStore + { + IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent? applyEvent); + + IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot? applySnapshot); + + IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot? applySnapshot, HandleEvent? applyEvent); + + ISnapshotStore GetSnapshotStore(); + } +} diff --git a/backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs b/backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs new file mode 100644 index 000000000..cd2fe904e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/IStreamNameResolver.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.States +{ + public interface IStreamNameResolver + { + string GetStreamName(Type aggregateType, string id); + + string WithNewId(string streamName, Func idGenerator); + } +} diff --git a/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs b/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs new file mode 100644 index 000000000..96ea73b9f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.States +{ + [Serializable] + public class InconsistentStateException : Exception + { + private readonly long currentVersion; + private readonly long expectedVersion; + + public long CurrentVersion + { + get { return currentVersion; } + } + + public long ExpectedVersion + { + get { return expectedVersion; } + } + + public InconsistentStateException(long currentVersion, long expectedVersion, Exception? inner = null) + : base(FormatMessage(currentVersion, expectedVersion), inner) + { + this.currentVersion = currentVersion; + + this.expectedVersion = expectedVersion; + } + + protected InconsistentStateException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + currentVersion = info.GetInt64(nameof(currentVersion)); + + expectedVersion = info.GetInt64(nameof(expectedVersion)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(currentVersion), currentVersion); + info.AddValue(nameof(expectedVersion), expectedVersion); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(long currentVersion, long expectedVersion) + { + return $"Requested version {expectedVersion}, but found {currentVersion}."; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/States/Persistence.cs b/backend/src/Squidex.Infrastructure/States/Persistence.cs new file mode 100644 index 000000000..e388cf7be --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/Persistence.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + internal sealed class Persistence : Persistence, IPersistence where TKey : notnull + { + public Persistence(TKey ownerKey, Type ownerType, + IEventStore eventStore, + IEventEnricher eventEnricher, + IEventDataFormatter eventDataFormatter, + ISnapshotStore snapshotStore, + IStreamNameResolver streamNameResolver, + HandleEvent? applyEvent) + : base(ownerKey, ownerType, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) + { + } + } +} diff --git a/src/Squidex.Infrastructure/States/PersistenceMode.cs b/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs similarity index 100% rename from src/Squidex.Infrastructure/States/PersistenceMode.cs rename to backend/src/Squidex.Infrastructure/States/PersistenceMode.cs diff --git a/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs new file mode 100644 index 000000000..6f828189c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -0,0 +1,241 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement + +namespace Squidex.Infrastructure.States +{ + internal class Persistence : IPersistence where TKey : notnull + { + private readonly TKey ownerKey; + private readonly Type ownerType; + private readonly ISnapshotStore snapshotStore; + private readonly IStreamNameResolver streamNameResolver; + private readonly IEventStore eventStore; + private readonly IEventEnricher eventEnricher; + private readonly IEventDataFormatter eventDataFormatter; + private readonly PersistenceMode persistenceMode; + private readonly HandleSnapshot? applyState; + private readonly HandleEvent? applyEvent; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; + private long version; + + public long Version + { + get { return version; } + } + + public Persistence(TKey ownerKey, Type ownerType, + IEventStore eventStore, + IEventEnricher eventEnricher, + IEventDataFormatter eventDataFormatter, + ISnapshotStore snapshotStore, + IStreamNameResolver streamNameResolver, + PersistenceMode persistenceMode, + HandleSnapshot? applyState, + HandleEvent? applyEvent) + { + this.ownerKey = ownerKey; + this.ownerType = ownerType; + this.applyState = applyState; + this.applyEvent = applyEvent; + this.eventStore = eventStore; + this.eventEnricher = eventEnricher; + this.eventDataFormatter = eventDataFormatter; + this.persistenceMode = persistenceMode; + this.snapshotStore = snapshotStore; + this.streamNameResolver = streamNameResolver; + } + + public async Task ReadAsync(long expectedVersion = EtagVersion.Any) + { + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; + + await ReadSnapshotAsync(); + await ReadEventsAsync(); + + UpdateVersion(); + + if (expectedVersion > EtagVersion.Any && expectedVersion != version) + { + if (version == EtagVersion.Empty) + { + throw new DomainObjectNotFoundException(ownerKey.ToString()!, ownerType); + } + else + { + throw new InconsistentStateException(version, expectedVersion); + } + } + } + + private async Task ReadSnapshotAsync() + { + if (UseSnapshots()) + { + var (state, position) = await snapshotStore.ReadAsync(ownerKey); + + if (position < EtagVersion.Empty) + { + position = EtagVersion.Empty; + } + + versionSnapshot = position; + versionEvents = position; + + if (applyState != null && position >= 0) + { + applyState(state); + } + } + } + + private async Task ReadEventsAsync() + { + if (UseEventSourcing()) + { + var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1); + + foreach (var @event in events) + { + versionEvents++; + + if (@event.EventStreamNumber != versionEvents) + { + throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + } + + var parsedEvent = ParseKnownEvent(@event); + + if (applyEvent != null && parsedEvent != null) + { + applyEvent(parsedEvent); + } + } + } + } + + public async Task WriteSnapshotAsync(TSnapshot state) + { + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; + + if (newVersion != versionSnapshot) + { + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); + + versionSnapshot = newVersion; + } + + UpdateVersion(); + } + + public async Task WriteEventsAsync(IEnumerable> events) + { + Guard.NotNull(events); + + var eventArray = events.ToArray(); + + if (eventArray.Length > 0) + { + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; + + var commitId = Guid.NewGuid(); + + foreach (var @event in eventArray) + { + eventEnricher.Enrich(@event, ownerKey); + } + + var eventStream = GetStreamName(); + var eventData = GetEventData(eventArray, commitId); + + try + { + await eventStore.AppendAsync(commitId, eventStream, expectedVersion, eventData); + } + catch (WrongEventVersionException ex) + { + throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); + } + + versionEvents += eventArray.Length; + } + + UpdateVersion(); + } + + public async Task DeleteAsync() + { + if (UseEventSourcing()) + { + await eventStore.DeleteStreamAsync(GetStreamName()); + } + + if (UseSnapshots()) + { + await snapshotStore.RemoveAsync(ownerKey); + } + } + + private EventData[] GetEventData(Envelope[] events, Guid commitId) + { + return events.Map(x => eventDataFormatter.ToEventData(x, commitId, true)); + } + + private string GetStreamName() + { + return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()!); + } + + private bool UseSnapshots() + { + return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private bool UseEventSourcing() + { + return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private Envelope? ParseKnownEvent(StoredEvent storedEvent) + { + try + { + return eventDataFormatter.Parse(storedEvent.Data); + } + catch (TypeNameNotFoundException) + { + return null; + } + } + + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; + } + else if (persistenceMode == PersistenceMode.EventSourcing) + { + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/States/Store.cs b/backend/src/Squidex.Infrastructure/States/Store.cs new file mode 100644 index 000000000..278df0a16 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/Store.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + public sealed class Store : IStore where TKey : notnull + { + private readonly IServiceProvider services; + private readonly IStreamNameResolver streamNameResolver; + private readonly IEventStore eventStore; + private readonly IEventEnricher eventEnricher; + private readonly IEventDataFormatter eventDataFormatter; + + public Store( + IEventStore eventStore, + IEventEnricher eventEnricher, + IEventDataFormatter eventDataFormatter, + IServiceProvider services, + IStreamNameResolver streamNameResolver) + { + this.eventStore = eventStore; + this.eventEnricher = eventEnricher; + this.eventDataFormatter = eventDataFormatter; + this.services = services; + this.streamNameResolver = streamNameResolver; + } + + public IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent? applyEvent) + { + return CreatePersistence(owner, key, applyEvent); + } + + public IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot? applySnapshot) + { + return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); + } + + public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot? applySnapshot, HandleEvent? applyEvent) + { + return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + } + + private IPersistence CreatePersistence(Type owner, TKey key, HandleEvent? applyEvent) + { + Guard.NotNull(key); + + var snapshotStore = GetSnapshotStore(); + + return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); + } + + private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, HandleSnapshot? applySnapshot, HandleEvent? applyEvent) + { + Guard.NotNull(key); + + var snapshotStore = GetSnapshotStore(); + + return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + } + + public ISnapshotStore GetSnapshotStore() + { + return (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + } + } +} diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/States/StoreExtensions.cs rename to backend/src/Squidex.Infrastructure/States/StoreExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs new file mode 100644 index 000000000..44f16583e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -0,0 +1,801 @@ +// ========================================================================== +// 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 System.Text; +using System.Text.RegularExpressions; + +namespace Squidex.Infrastructure +{ + public static class StringExtensions + { + private const char NullChar = (char)0; + + private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); + private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); + private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); + + private static readonly Dictionary LowerCaseDiacritics; + private static readonly Dictionary Diacritics = new Dictionary + { + ['$'] = "dollar", + ['%'] = "percent", + ['&'] = "and", + ['<'] = "less", + ['>'] = "greater", + ['|'] = "or", + ['¢'] = "cent", + ['£'] = "pound", + ['¤'] = "currency", + ['¥'] = "yen", + ['©'] = "(c)", + ['ª'] = "a", + ['®'] = "(r)", + ['º'] = "o", + ['À'] = "A", + ['Á'] = "A", + ['Â'] = "A", + ['Ã'] = "A", + ['Ä'] = "AE", + ['Å'] = "A", + ['Æ'] = "AE", + ['Ç'] = "C", + ['Ə'] = "E", + ['È'] = "E", + ['É'] = "E", + ['Ê'] = "E", + ['Ë'] = "E", + ['Ì'] = "I", + ['Í'] = "I", + ['Î'] = "I", + ['Ï'] = "I", + ['Ð'] = "D", + ['Ñ'] = "N", + ['Ò'] = "O", + ['Ó'] = "O", + ['Ô'] = "O", + ['Õ'] = "O", + ['Ö'] = "OE", + ['Ø'] = "O", + ['Ù'] = "U", + ['Ú'] = "U", + ['Û'] = "U", + ['Ü'] = "UE", + ['Ý'] = "Y", + ['Þ'] = "TH", + ['ß'] = "ss", + ['à'] = "a", + ['á'] = "a", + ['â'] = "a", + ['ã'] = "a", + ['ä'] = "ae", + ['å'] = "a", + ['æ'] = "ae", + ['ç'] = "c", + ['ə'] = "e", + ['è'] = "e", + ['é'] = "e", + ['ê'] = "e", + ['ë'] = "e", + ['ì'] = "i", + ['í'] = "i", + ['î'] = "i", + ['ï'] = "i", + ['ð'] = "d", + ['ñ'] = "n", + ['ò'] = "o", + ['ó'] = "o", + ['ô'] = "o", + ['õ'] = "o", + ['ö'] = "oe", + ['ø'] = "o", + ['ù'] = "u", + ['ú'] = "u", + ['û'] = "u", + ['ü'] = "ue", + ['ý'] = "y", + ['þ'] = "th", + ['ÿ'] = "y", + ['Ā'] = "A", + ['ā'] = "a", + ['Ă'] = "A", + ['ă'] = "a", + ['Ą'] = "A", + ['ą'] = "a", + ['Ć'] = "C", + ['ć'] = "c", + ['Č'] = "C", + ['č'] = "c", + ['Ď'] = "D", + ['ď'] = "d", + ['Đ'] = "DJ", + ['đ'] = "dj", + ['Ē'] = "E", + ['ē'] = "e", + ['Ė'] = "E", + ['ė'] = "e", + ['Ę'] = "e", + ['ę'] = "e", + ['Ě'] = "E", + ['ě'] = "e", + ['Ğ'] = "G", + ['ğ'] = "g", + ['Ģ'] = "G", + ['ģ'] = "g", + ['Ĩ'] = "I", + ['ĩ'] = "i", + ['Ī'] = "i", + ['ī'] = "i", + ['Į'] = "I", + ['į'] = "i", + ['İ'] = "I", + ['ı'] = "i", + ['Ķ'] = "k", + ['ķ'] = "k", + ['Ļ'] = "L", + ['ļ'] = "l", + ['Ľ'] = "L", + ['ľ'] = "l", + ['Ł'] = "L", + ['ł'] = "l", + ['Ń'] = "N", + ['ń'] = "n", + ['Ņ'] = "N", + ['ņ'] = "n", + ['Ň'] = "N", + ['ň'] = "n", + ['Ő'] = "O", + ['ő'] = "o", + ['Œ'] = "OE", + ['œ'] = "oe", + ['Ŕ'] = "R", + ['ŕ'] = "r", + ['Ř'] = "R", + ['ř'] = "r", + ['Ś'] = "S", + ['ś'] = "s", + ['Ş'] = "S", + ['ş'] = "s", + ['Š'] = "S", + ['š'] = "s", + ['Ţ'] = "T", + ['ţ'] = "t", + ['Ť'] = "T", + ['ť'] = "t", + ['Ũ'] = "U", + ['ũ'] = "u", + ['Ū'] = "u", + ['ū'] = "u", + ['Ů'] = "U", + ['ů'] = "u", + ['Ű'] = "U", + ['ű'] = "u", + ['Ų'] = "U", + ['ų'] = "u", + ['Ź'] = "Z", + ['ź'] = "z", + ['Ż'] = "Z", + ['ż'] = "z", + ['Ž'] = "Z", + ['ž'] = "z", + ['ƒ'] = "f", + ['Ơ'] = "O", + ['ơ'] = "o", + ['Ư'] = "U", + ['ư'] = "u", + ['Lj'] = "LJ", + ['lj'] = "lj", + ['Nj'] = "NJ", + ['nj'] = "nj", + ['Ș'] = "S", + ['ș'] = "s", + ['Ț'] = "T", + ['ț'] = "t", + ['˚'] = "o", + ['Ά'] = "A", + ['Έ'] = "E", + ['Ή'] = "H", + ['Ί'] = "I", + ['Ό'] = "O", + ['Ύ'] = "Y", + ['Ώ'] = "W", + ['ΐ'] = "i", + ['Α'] = "A", + ['Β'] = "B", + ['Γ'] = "G", + ['Δ'] = "D", + ['Ε'] = "E", + ['Ζ'] = "Z", + ['Η'] = "H", + ['Θ'] = "8", + ['Ι'] = "I", + ['Κ'] = "K", + ['Λ'] = "L", + ['Μ'] = "M", + ['Ν'] = "N", + ['Ξ'] = "3", + ['Ο'] = "O", + ['Π'] = "P", + ['Ρ'] = "R", + ['Σ'] = "S", + ['Τ'] = "T", + ['Υ'] = "Y", + ['Φ'] = "F", + ['Χ'] = "X", + ['Ψ'] = "PS", + ['Ω'] = "W", + ['Ϊ'] = "I", + ['Ϋ'] = "Y", + ['ά'] = "a", + ['έ'] = "e", + ['ή'] = "h", + ['ί'] = "i", + ['ΰ'] = "y", + ['α'] = "a", + ['β'] = "b", + ['γ'] = "g", + ['δ'] = "d", + ['ε'] = "e", + ['ζ'] = "z", + ['η'] = "h", + ['θ'] = "8", + ['ι'] = "i", + ['κ'] = "k", + ['λ'] = "l", + ['μ'] = "m", + ['ν'] = "n", + ['ξ'] = "3", + ['ο'] = "o", + ['π'] = "p", + ['ρ'] = "r", + ['ς'] = "s", + ['σ'] = "s", + ['τ'] = "t", + ['υ'] = "y", + ['φ'] = "f", + ['χ'] = "x", + ['ψ'] = "ps", + ['ω'] = "w", + ['ϊ'] = "i", + ['ϋ'] = "y", + ['ό'] = "o", + ['ύ'] = "y", + ['ώ'] = "w", + ['Ё'] = "Yo", + ['Ђ'] = "DJ", + ['Є'] = "Ye", + ['І'] = "I", + ['Ї'] = "Yi", + ['Ј'] = "J", + ['Љ'] = "LJ", + ['Њ'] = "NJ", + ['Ћ'] = "C", + ['Џ'] = "DZ", + ['А'] = "A", + ['Б'] = "B", + ['В'] = "V", + ['Г'] = "G", + ['Д'] = "D", + ['Е'] = "E", + ['Ж'] = "Zh", + ['З'] = "Z", + ['И'] = "I", + ['Й'] = "J", + ['К'] = "K", + ['Л'] = "L", + ['М'] = "M", + ['Н'] = "N", + ['О'] = "O", + ['П'] = "P", + ['Р'] = "R", + ['С'] = "S", + ['Т'] = "T", + ['У'] = "U", + ['Ф'] = "F", + ['Х'] = "H", + ['Ц'] = "C", + ['Ч'] = "Ch", + ['Ш'] = "Sh", + ['Щ'] = "Sh", + ['Ъ'] = "U", + ['Ы'] = "Y", + ['Ь'] = "b", + ['Э'] = "E", + ['Ю'] = "Yu", + ['Я'] = "Ya", + ['а'] = "a", + ['б'] = "b", + ['в'] = "v", + ['г'] = "g", + ['д'] = "d", + ['е'] = "e", + ['ж'] = "zh", + ['з'] = "z", + ['и'] = "i", + ['й'] = "j", + ['к'] = "k", + ['л'] = "l", + ['м'] = "m", + ['н'] = "n", + ['о'] = "o", + ['п'] = "p", + ['р'] = "r", + ['с'] = "s", + ['т'] = "t", + ['у'] = "u", + ['ф'] = "f", + ['х'] = "h", + ['ц'] = "c", + ['ч'] = "ch", + ['ш'] = "sh", + ['щ'] = "sh", + ['ъ'] = "u", + ['ы'] = "y", + ['ь'] = "s", + ['э'] = "e", + ['ю'] = "yu", + ['я'] = "ya", + ['ё'] = "yo", + ['ђ'] = "dj", + ['є'] = "ye", + ['і'] = "i", + ['ї'] = "yi", + ['ј'] = "j", + ['љ'] = "lj", + ['њ'] = "nj", + ['ћ'] = "c", + ['џ'] = "dz", + ['Ґ'] = "G", + ['ґ'] = "g", + ['฿'] = "baht", + ['ა'] = "a", + ['ბ'] = "b", + ['გ'] = "g", + ['დ'] = "d", + ['ე'] = "e", + ['ვ'] = "v", + ['ზ'] = "z", + ['თ'] = "t", + ['ი'] = "i", + ['კ'] = "k", + ['ლ'] = "l", + ['მ'] = "m", + ['ნ'] = "n", + ['ო'] = "o", + ['პ'] = "p", + ['ჟ'] = "zh", + ['რ'] = "r", + ['ს'] = "s", + ['ტ'] = "t", + ['უ'] = "u", + ['ფ'] = "f", + ['ქ'] = "k", + ['ღ'] = "gh", + ['ყ'] = "q", + ['შ'] = "sh", + ['ჩ'] = "ch", + ['ც'] = "ts", + ['ძ'] = "dz", + ['წ'] = "ts", + ['ჭ'] = "ch", + ['ხ'] = "kh", + ['ჯ'] = "j", + ['ჰ'] = "h", + ['ẞ'] = "SS", + ['Ạ'] = "A", + ['ạ'] = "a", + ['Ả'] = "A", + ['ả'] = "a", + ['Ấ'] = "A", + ['ấ'] = "a", + ['Ầ'] = "A", + ['ầ'] = "a", + ['Ẩ'] = "A", + ['ẩ'] = "a", + ['Ẫ'] = "A", + ['ẫ'] = "a", + ['Ậ'] = "A", + ['ậ'] = "a", + ['Ắ'] = "A", + ['ắ'] = "a", + ['Ằ'] = "A", + ['ằ'] = "a", + ['Ẳ'] = "A", + ['ẳ'] = "a", + ['Ẵ'] = "A", + ['ẵ'] = "a", + ['Ặ'] = "A", + ['ặ'] = "a", + ['Ẹ'] = "E", + ['ẹ'] = "e", + ['Ẻ'] = "E", + ['ẻ'] = "e", + ['Ẽ'] = "E", + ['ẽ'] = "e", + ['Ế'] = "E", + ['ế'] = "e", + ['Ề'] = "E", + ['ề'] = "e", + ['Ể'] = "E", + ['ể'] = "e", + ['Ễ'] = "E", + ['ễ'] = "e", + ['Ệ'] = "E", + ['ệ'] = "e", + ['Ỉ'] = "I", + ['ỉ'] = "i", + ['Ị'] = "I", + ['ị'] = "i", + ['Ọ'] = "O", + ['ọ'] = "o", + ['Ỏ'] = "O", + ['ỏ'] = "o", + ['Ố'] = "O", + ['ố'] = "o", + ['Ồ'] = "O", + ['ồ'] = "o", + ['Ổ'] = "O", + ['ổ'] = "o", + ['Ỗ'] = "O", + ['ỗ'] = "o", + ['Ộ'] = "O", + ['ộ'] = "o", + ['Ớ'] = "O", + ['ớ'] = "o", + ['Ờ'] = "O", + ['ờ'] = "o", + ['Ở'] = "O", + ['ở'] = "o", + ['Ỡ'] = "O", + ['ỡ'] = "o", + ['Ợ'] = "O", + ['ợ'] = "o", + ['Ụ'] = "U", + ['ụ'] = "u", + ['Ủ'] = "U", + ['ủ'] = "u", + ['Ứ'] = "U", + ['ứ'] = "u", + ['Ừ'] = "U", + ['ừ'] = "u", + ['Ử'] = "U", + ['ử'] = "u", + ['Ữ'] = "U", + ['ữ'] = "u", + ['Ự'] = "U", + ['ự'] = "u", + ['Ỳ'] = "Y", + ['ỳ'] = "y", + ['Ỵ'] = "Y", + ['ỵ'] = "y", + ['Ỷ'] = "Y", + ['ỷ'] = "y", + ['Ỹ'] = "Y", + ['ỹ'] = "y", + ['‘'] = "\'", + ['’'] = "\'", + ['“'] = "\\\"", + ['”'] = "\\\"", + ['†'] = "+", + ['•'] = "*", + ['…'] = "...", + ['₠'] = "ecu", + ['₢'] = "cruzeiro", + ['₣'] = "french franc", + ['₤'] = "lira", + ['₥'] = "mill", + ['₦'] = "naira", + ['₧'] = "peseta", + ['₨'] = "rupee", + ['₩'] = "won", + ['₪'] = "new shequel", + ['₫'] = "dong", + ['€'] = "euro", + ['₭'] = "kip", + ['₮'] = "tugrik", + ['₯'] = "drachma", + ['₰'] = "penny", + ['₱'] = "peso", + ['₲'] = "guarani", + ['₳'] = "austral", + ['₴'] = "hryvnia", + ['₵'] = "cedi", + ['₹'] = "indian rupee", + ['₽'] = "russian ruble", + ['₿'] = "bitcoin", + ['℠'] = "sm", + ['™'] = "tm", + ['∂'] = "d", + ['∆'] = "delta", + ['∑'] = "sum", + ['∞'] = "infinity", + ['♥'] = "love", + ['元'] = "yuan", + ['円'] = "yen", + ['﷼'] = "rial" + }; + + static StringExtensions() + { + LowerCaseDiacritics = Diacritics.ToDictionary(x => x.Key, x => x.Value.ToLowerInvariant()); + } + + public static bool IsSlug(this string? value) + { + return value != null && SlugRegex.IsMatch(value); + } + + public static bool IsEmail(this string? value) + { + return value != null && EmailRegex.IsMatch(value); + } + + public static bool IsPropertyName(this string? value) + { + return value != null && PropertyNameRegex.IsMatch(value); + } + + public static string WithFallback(this string? value, string fallback) + { + return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; + } + + public static string ToPascalCase(this string value) + { + if (value.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(value.Length); + + var last = NullChar; + var length = 0; + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '-' || c == '_' || c == ' ') + { + if (last != NullChar) + { + sb.Append(char.ToUpperInvariant(last)); + } + + last = NullChar; + length = 0; + } + else + { + if (length > 1) + { + sb.Append(c); + } + else if (length == 0) + { + last = c; + } + else + { + sb.Append(char.ToUpperInvariant(last)); + sb.Append(c); + + last = NullChar; + } + + length++; + } + } + + if (last != NullChar) + { + sb.Append(char.ToUpperInvariant(last)); + } + + return sb.ToString(); + } + + public static string ToKebabCase(this string value) + { + if (value.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(value.Length); + + var length = 0; + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '-' || c == '_' || c == ' ') + { + length = 0; + } + else + { + if (length > 0) + { + sb.Append(char.ToLowerInvariant(c)); + } + else + { + if (sb.Length > 0) + { + sb.Append('-'); + } + + sb.Append(char.ToLowerInvariant(c)); + } + + length++; + } + } + + return sb.ToString(); + } + + public static string ToCamelCase(this string value) + { + if (value.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(value.Length); + + var last = NullChar; + var length = 0; + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '-' || c == '_' || c == ' ') + { + if (last != NullChar) + { + if (sb.Length > 0) + { + sb.Append(char.ToUpperInvariant(last)); + } + else + { + sb.Append(char.ToLowerInvariant(last)); + } + } + + last = NullChar; + length = 0; + } + else + { + if (length > 1) + { + sb.Append(c); + } + else if (length == 0) + { + last = c; + } + else + { + if (sb.Length > 0) + { + sb.Append(char.ToUpperInvariant(last)); + } + else + { + sb.Append(char.ToLowerInvariant(last)); + } + + sb.Append(c); + + last = NullChar; + } + + length++; + } + } + + if (last != NullChar) + { + if (sb.Length > 0) + { + sb.Append(char.ToUpperInvariant(last)); + } + else + { + sb.Append(char.ToLowerInvariant(last)); + } + } + + return sb.ToString(); + } + + public static string Slugify(this string value, ISet? preserveHash = null, bool singleCharDiactric = false, char separator = '-') + { + var result = new StringBuilder(value.Length); + + var lastChar = (char)0; + + for (var i = 0; i < value.Length; i++) + { + var character = value[i]; + + if (preserveHash?.Contains(character) == true) + { + result.Append(character); + } + else if (char.IsLetter(character) || char.IsNumber(character)) + { + lastChar = character; + + var lower = char.ToLowerInvariant(character); + + if (LowerCaseDiacritics.TryGetValue(character, out var replacement)) + { + if (singleCharDiactric && replacement.Length == 2) + { + result.Append(replacement[0]); + } + else + { + result.Append(replacement); + } + } + else + { + result.Append(lower); + } + } + else if ((i < value.Length - 1) && (i > 0 && lastChar != separator)) + { + lastChar = separator; + + result.Append(separator); + } + } + + return result.ToString().Trim(separator); + } + + public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) + { + Guard.NotNull(path); + + var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; + + if (trailingSlash && + url.IndexOf("#", StringComparison.OrdinalIgnoreCase) < 0 && + url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 && + url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0) + { + url += "/"; + } + + return url; + } + + public static string JoinNonEmpty(string separator, params string?[] parts) + { + Guard.NotNull(separator); + + if (parts == null || parts.Length == 0) + { + return string.Empty; + } + + return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs new file mode 100644 index 000000000..f7127be20 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLocalCleaner : IDisposable + { + private readonly AsyncLocal asyncLocal; + + public AsyncLocalCleaner(AsyncLocal asyncLocal) + { + Guard.NotNull(asyncLocal); + + this.asyncLocal = asyncLocal; + } + + public void Dispose() + { + asyncLocal.Value = default!; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs new file mode 100644 index 000000000..3425a60b6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLock + { + private readonly SemaphoreSlim semaphore; + + public AsyncLock() + { + semaphore = new SemaphoreSlim(1); + } + + public Task LockAsync() + { + var wait = semaphore.WaitAsync(); + + if (wait.IsCompleted) + { + return Task.FromResult((IDisposable)new LockReleaser(this)); + } + else + { + return wait.ContinueWith(x => (IDisposable)new LockReleaser(this), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + private class LockReleaser : IDisposable + { + private AsyncLock? target; + + internal LockReleaser(AsyncLock target) + { + this.target = target; + } + + public void Dispose() + { + var current = target; + + if (current == null) + { + return; + } + + target = null; + + try + { + current.semaphore.Release(); + } + catch + { + // just ignore the Exception + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs new file mode 100644 index 000000000..8ec96d9b6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLockPool + { + private readonly AsyncLock[] locks; + + public AsyncLockPool(int poolSize) + { + Guard.GreaterThan(poolSize, 0); + + locks = new AsyncLock[poolSize]; + + for (var i = 0; i < poolSize; i++) + { + locks[i] = new AsyncLock(); + } + } + + public Task LockAsync(object target) + { + Guard.NotNull(target); + + return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs new file mode 100644 index 000000000..7b89f8e0d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.Tasks +{ + public class PartitionedActionBlock : ITargetBlock + { + private readonly ITargetBlock distributor; + private readonly ActionBlock[] workers; + + public Task Completion + { + get { return Task.WhenAll(workers.Select(x => x.Completion)); } + } + + public PartitionedActionBlock(Action action, Func partitioner) + : this (action?.ToAsync()!, partitioner, new ExecutionDataflowBlockOptions()) + { + } + + public PartitionedActionBlock(Func action, Func partitioner) + : this(action, partitioner, new ExecutionDataflowBlockOptions()) + { + } + + public PartitionedActionBlock(Action action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) + : this(action?.ToAsync()!, partitioner, dataflowBlockOptions) + { + } + + public PartitionedActionBlock(Func action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) + { + Guard.NotNull(action); + Guard.NotNull(partitioner); + Guard.NotNull(dataflowBlockOptions); + Guard.GreaterThan(dataflowBlockOptions.MaxDegreeOfParallelism, 1); + + workers = new ActionBlock[dataflowBlockOptions.MaxDegreeOfParallelism]; + + for (var i = 0; i < dataflowBlockOptions.MaxDegreeOfParallelism; i++) + { + var workerOption = SimpleMapper.Map(dataflowBlockOptions, new ExecutionDataflowBlockOptions()); + + workerOption.MaxDegreeOfParallelism = 1; + workerOption.MaxMessagesPerTask = 1; + + workers[i] = new ActionBlock(action, workerOption); + } + + var distributorOption = new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + BoundedCapacity = 1 + }; + + distributor = new ActionBlock(x => + { + var partition = Math.Abs(partitioner(x)) % workers.Length; + + return workers[partition].SendAsync(x); + }, distributorOption); + + distributor.Completion.ContinueWith(x => + { + foreach (var worker in workers) + { + worker.Complete(); + } + }); + } + + public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock source, bool consumeToAccept) + { + return distributor.OfferMessage(messageHeader, messageValue, source, consumeToAccept); + } + + public void Complete() + { + distributor.Complete(); + } + + public void Fault(Exception exception) + { + distributor.Fault(exception); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs b/backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs new file mode 100644 index 000000000..244f2c96e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class SingleThreadedDispatcher + { + private readonly ActionBlock> block; + private bool isStopped; + + public SingleThreadedDispatcher(int capacity = 1) + { + var options = new ExecutionDataflowBlockOptions + { + BoundedCapacity = capacity, + MaxMessagesPerTask = 1, + MaxDegreeOfParallelism = 1 + }; + + block = new ActionBlock>(Handle, options); + } + + public Task DispatchAsync(Func action) + { + Guard.NotNull(action); + + return block.SendAsync(action); + } + + public Task DispatchAsync(Action action) + { + Guard.NotNull(action); + + return block.SendAsync(() => { action(); return TaskHelper.Done; }); + } + + public async Task StopAndWaitAsync() + { + await DispatchAsync(() => + { + isStopped = true; + + block.Complete(); + }); + + await block.Completion; + } + + private Task Handle(Func action) + { + if (isStopped) + { + return TaskHelper.Done; + } + + return action(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs new file mode 100644 index 000000000..4078d8e93 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public static class TaskExtensions + { + private static readonly Action IgnoreTaskContinuation = t => { var ignored = t.Exception; }; + + public static void Forget(this Task task) + { + if (task.IsCompleted) + { +#pragma warning disable IDE0059 // Unnecessary assignment of a value + var ignored = task.Exception; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + } + else + { + task.ContinueWith( + IgnoreTaskContinuation, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted | + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + public static Func ToDefault(this Action action) + { + Guard.NotNull(action); + + return x => + { + action(x); + + return default!; + }; + } + + public static Func> ToDefault(this Func action) + { + Guard.NotNull(action); + + return async x => + { + await action(x); + + return default!; + }; + } + + public static Func> ToAsync(this Func action) + { + Guard.NotNull(action); + + return x => + { + var result = action(x); + + return Task.FromResult(result); + }; + } + + public static Func ToAsync(this Action action) + { + return x => + { + action(x); + + return TaskHelper.Done; + }; + } + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (cancellationToken.Register(state => + { + ((TaskCompletionSource)state!).TrySetResult(null!); + }, + tcs)) + { + var resultTask = await Task.WhenAny(task, tcs.Task); + if (resultTask == tcs.Task) + { + throw new OperationCanceledException(cancellationToken); + } + + return await task; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs new file mode 100644 index 000000000..6e3ca6f64 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskHelper.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public static class TaskHelper + { + public static readonly Task Done = CreateDoneTask(); + public static readonly Task False = CreateResultTask(false); + public static readonly Task True = CreateResultTask(true); + + private static Task CreateDoneTask() + { + var result = new TaskCompletionSource(); + + result.SetResult(null); + + return result.Task; + } + + private static Task CreateResultTask(bool value) + { + var result = new TaskCompletionSource(); + + result.SetResult(value); + + return result.Task; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs new file mode 100644 index 000000000..ee865d5a2 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Timers +{ + public sealed class CompletionTimer + { + private const int OneCallNotExecuted = 0; + private const int OneCallExecuted = 1; + private const int OneCallRequested = 2; + private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); + private readonly Task runTask; + private int oneCallState; + private CancellationTokenSource? wakeupToken; + + public CompletionTimer(int delayInMs, Func callback, int initialDelay = 0) + { + Guard.NotNull(callback); + Guard.GreaterThan(delayInMs, 0); + + runTask = RunInternalAsync(delayInMs, initialDelay, callback); + } + + public Task StopAsync() + { + stopToken.Cancel(); + + return runTask; + } + + public void SkipCurrentDelay() + { + if (!stopToken.IsCancellationRequested) + { + Interlocked.CompareExchange(ref oneCallState, OneCallRequested, OneCallNotExecuted); + + wakeupToken?.Cancel(); + } + } + + private async Task RunInternalAsync(int delay, int initialDelay, Func callback) + { + try + { + if (initialDelay > 0) + { + await WaitAsync(initialDelay).ConfigureAwait(false); + } + + while (oneCallState == OneCallRequested || !stopToken.IsCancellationRequested) + { + await callback(stopToken.Token).ConfigureAwait(false); + + oneCallState = OneCallExecuted; + + await WaitAsync(delay).ConfigureAwait(false); + } + } + catch + { + return; + } + } + + private async Task WaitAsync(int intervall) + { + try + { + wakeupToken = new CancellationTokenSource(); + + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, wakeupToken.Token)) + { + await Task.Delay(intervall, cts.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs new file mode 100644 index 000000000..6c7af39f7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Json; + +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + +namespace Squidex.Infrastructure.Translations +{ + public sealed class DeepLTranslator : ITranslator + { + private const string Url = "https://api.deepl.com/v2/translate"; + private readonly HttpClient httpClient = new HttpClient(); + private readonly DeepLTranslatorOptions deepLOptions; + private readonly IJsonSerializer jsonSerializer; + + private sealed class Response + { + public ResponseTranslation[] Translations { get; set; } + } + + private sealed class ResponseTranslation + { + public string Text { get; set; } + } + + public DeepLTranslator(IOptions deepLOptions, IJsonSerializer jsonSerializer) + { + Guard.NotNull(deepLOptions); + Guard.NotNull(jsonSerializer); + + this.deepLOptions = deepLOptions.Value; + + this.jsonSerializer = jsonSerializer; + } + + public async Task Translate(string sourceText, Language targetLanguage, Language? sourceLanguage = null, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(sourceText) || targetLanguage == null) + { + return new Translation(TranslationResult.NotTranslated, sourceText); + } + + if (string.IsNullOrWhiteSpace(deepLOptions.AuthKey)) + { + return new Translation(TranslationResult.NotImplemented); + } + + var parameters = new Dictionary + { + ["auth_key"] = deepLOptions.AuthKey, + ["text"] = sourceText, + ["target_lang"] = GetLanguageCode(targetLanguage) + }; + + if (sourceLanguage != null) + { + parameters["source_lang"] = GetLanguageCode(sourceLanguage); + } + + var response = await httpClient.PostAsync(Url, new FormUrlEncodedContent(parameters), ct); + var responseString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var result = jsonSerializer.Deserialize(responseString); + + if (result?.Translations?.Length == 1) + { + return new Translation(TranslationResult.Translated, result.Translations[0].Text); + } + } + + if (response.StatusCode == HttpStatusCode.BadRequest) + { + return new Translation(TranslationResult.LanguageNotSupported, resultText: responseString); + } + + return new Translation(TranslationResult.Failed, resultText: responseString); + } + + private static string GetLanguageCode(Language language) + { + return language.Iso2Code.Substring(0, 2).ToUpperInvariant(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs new file mode 100644 index 000000000..653e3062d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public sealed class DeepLTranslatorOptions + { + public string? AuthKey { get; set; } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/ITranslator.cs b/backend/src/Squidex.Infrastructure/Translations/ITranslator.cs new file mode 100644 index 000000000..84f6df5df --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/ITranslator.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Translations +{ + public interface ITranslator + { + Task Translate(string sourceText, Language targetLanguage, Language? sourceLanguage = null, CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs b/backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs new file mode 100644 index 000000000..567d167fb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/NoopTranslator.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Translations +{ + public sealed class NoopTranslator : ITranslator + { + public Task Translate(string sourceText, Language targetLanguage, Language? sourceLanguage = null, CancellationToken ct = default) + { + var result = new Translation(TranslationResult.NotImplemented); + + return Task.FromResult(result); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Translations/Translation.cs b/backend/src/Squidex.Infrastructure/Translations/Translation.cs new file mode 100644 index 000000000..619259006 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Translations/Translation.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public sealed class Translation + { + public TranslationResult Result { get; } + + public string? Text { get; } + + public string? ResultText { get; set; } + + public Translation(TranslationResult result, string? text = null, string? resultText = null) + { + Text = text; + Result = result; + ResultText = resultText; + } + } +} diff --git a/src/Squidex.Infrastructure/Translations/TranslationResult.cs b/backend/src/Squidex.Infrastructure/Translations/TranslationResult.cs similarity index 100% rename from src/Squidex.Infrastructure/Translations/TranslationResult.cs rename to backend/src/Squidex.Infrastructure/Translations/TranslationResult.cs diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs new file mode 100644 index 000000000..fd10160fe --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker + { + public const string CounterTotalCalls = "TotalCalls"; + public const string CounterTotalElapsedMs = "TotalElapsedMs"; + + private const string FallbackCategory = "*"; + private const int Intervall = 60 * 1000; + private readonly IUsageRepository usageRepository; + private readonly ISemanticLog log; + private readonly CompletionTimer timer; + private ConcurrentDictionary<(string Key, string Category), Usage> usages = new ConcurrentDictionary<(string Key, string Category), Usage>(); + + public BackgroundUsageTracker(IUsageRepository usageRepository, ISemanticLog log) + { + Guard.NotNull(usageRepository); + Guard.NotNull(log); + + this.usageRepository = usageRepository; + + this.log = log; + + timer = new CompletionTimer(Intervall, ct => TrackAsync(), Intervall); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + timer.StopAsync().Wait(); + } + } + + public void Next() + { + ThrowIfDisposed(); + + timer.SkipCurrentDelay(); + } + + private async Task TrackAsync() + { + try + { + var today = DateTime.Today; + + var localUsages = Interlocked.Exchange(ref usages, new ConcurrentDictionary<(string Key, string Category), Usage>()); + + if (localUsages.Count > 0) + { + var updates = new UsageUpdate[localUsages.Count]; + var updateIndex = 0; + + foreach (var kvp in localUsages) + { + var counters = new Counters + { + [CounterTotalCalls] = kvp.Value.Count, + [CounterTotalElapsedMs] = kvp.Value.ElapsedMs + }; + + updates[updateIndex].Key = kvp.Key.Key; + updates[updateIndex].Category = kvp.Key.Category; + updates[updateIndex].Counters = counters; + updates[updateIndex].Date = today; + + updateIndex++; + } + + await usageRepository.TrackUsagesAsync(updates); + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "TrackUsage") + .WriteProperty("status", "Failed")); + } + } + + public Task TrackAsync(string key, string? category, double weight, double elapsedMs) + { + key = GetKey(key); + + ThrowIfDisposed(); + + if (weight > 0) + { + category = GetCategory(category); + + usages.AddOrUpdate((key, category), _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight)); + } + + return TaskHelper.Done; + } + + public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + key = GetKey(key); + + ThrowIfDisposed(); + + var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate); + var usagesByCategory = usagesFlat.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); + + var result = new Dictionary>(); + + IEnumerable categories = usagesByCategory.Keys; + + if (usagesByCategory.Count == 0) + { + var enriched = new List(); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + enriched.Add(new DateUsage(date, 0, 0)); + } + + result[FallbackCategory] = enriched; + } + else + { + foreach (var category in categories) + { + var enriched = new List(); + + var usagesDictionary = usagesByCategory[category].ToDictionary(x => x.Date); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var stored = usagesDictionary.GetOrDefault(date); + + var totalCount = 0L; + var totalElapsedMs = 0L; + + if (stored != null) + { + totalCount = (long)stored.Counters.Get(CounterTotalCalls); + totalElapsedMs = (long)stored.Counters.Get(CounterTotalElapsedMs); + } + + enriched.Add(new DateUsage(date, totalCount, totalElapsedMs)); + } + + result[category] = enriched; + } + } + + return result; + } + + public Task GetMonthlyCallsAsync(string key, DateTime date) + { + return GetPreviousCallsAsync(key, new DateTime(date.Year, date.Month, 1), date); + } + + public async Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) + { + key = GetKey(key); + + ThrowIfDisposed(); + + var originalUsages = await usageRepository.QueryAsync(key, fromDate, toDate); + + return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); + } + + private static string GetCategory(string? category) + { + return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; + } + + private static string GetKey(string key) + { + Guard.NotNull(key); + + return $"{key}_API"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs new file mode 100644 index 000000000..5d17385e4 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + private readonly IUsageTracker inner; + + public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) + : base(cache) + { + Guard.NotNull(inner); + + this.inner = inner; + } + + public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + Guard.NotNull(key); + + return inner.QueryAsync(key, fromDate, toDate); + } + + public Task TrackAsync(string key, string? category, double weight, double elapsedMs) + { + Guard.NotNull(key); + + return inner.TrackAsync(key, category, weight, elapsedMs); + } + + public Task GetMonthlyCallsAsync(string key, DateTime date) + { + Guard.NotNull(key); + + var cacheKey = string.Join("$", "Usage", nameof(GetMonthlyCallsAsync), key, date); + + return Cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return inner.GetMonthlyCallsAsync(key, date); + }); + } + + public Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) + { + Guard.NotNull(key); + + var cacheKey = string.Join("$", "Usage", nameof(GetPreviousCallsAsync), key, fromDate, toDate); + + return Cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return inner.GetPreviousCallsAsync(key, fromDate, toDate); + }); + } + } +} diff --git a/src/Squidex.Infrastructure/UsageTracking/Counters.cs b/backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/Counters.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs diff --git a/src/Squidex.Infrastructure/UsageTracking/DateUsage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/DateUsage.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/DateUsage.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/DateUsage.cs diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs new file mode 100644 index 000000000..772e82ba1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// 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.Threading.Tasks; + +namespace Squidex.Infrastructure.UsageTracking +{ + public interface IUsageTracker + { + Task TrackAsync(string key, string? category, double weight, double elapsedMs); + + Task GetMonthlyCallsAsync(string key, DateTime date); + + Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate); + + Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); + } +} diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs new file mode 100644 index 000000000..682c48577 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class StoredUsage + { + public string? Category { get; } + + public DateTime Date { get; } + + public Counters Counters { get; } + + public StoredUsage(string? category, DateTime date, Counters counters) + { + Guard.NotNull(counters); + + Category = category; + Counters = counters; + + Date = date; + } + } +} diff --git a/src/Squidex.Infrastructure/UsageTracking/Usage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/Usage.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/Usage.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/Usage.cs diff --git a/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs b/backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs similarity index 100% rename from src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs rename to backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs diff --git a/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs rename to backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs diff --git a/src/Squidex.Infrastructure/Validation/IValidatable.cs b/backend/src/Squidex.Infrastructure/Validation/IValidatable.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/IValidatable.cs rename to backend/src/Squidex.Infrastructure/Validation/IValidatable.cs diff --git a/src/Squidex.Infrastructure/Validation/Not.cs b/backend/src/Squidex.Infrastructure/Validation/Not.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/Not.cs rename to backend/src/Squidex.Infrastructure/Validation/Not.cs diff --git a/backend/src/Squidex.Infrastructure/Validation/Validate.cs b/backend/src/Squidex.Infrastructure/Validation/Validate.cs new file mode 100644 index 000000000..95e3a8db5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/Validate.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.Threading.Tasks; + +namespace Squidex.Infrastructure.Validation +{ + public static class Validate + { + public static void It(Func message, Action action) + { + List? errors = null; + + var addValidation = new AddValidation((m, p) => + { + if (errors == null) + { + errors = new List(); + } + + errors.Add(new ValidationError(m, p)); + }); + + action(addValidation); + + if (errors != null) + { + throw new ValidationException(message(), errors); + } + } + + public static async Task It(Func message, Func action) + { + List? errors = null; + + var addValidation = new AddValidation((m, p) => + { + if (errors == null) + { + errors = new List(); + } + + errors.Add(new ValidationError(m, p)); + }); + + await action(addValidation); + + if (errors != null) + { + throw new ValidationException(message(), errors); + } + } + } + + public delegate void AddValidation(string message, params string[] propertyNames); +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs new file mode 100644 index 000000000..7de0d81ef --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// 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; + +namespace Squidex.Infrastructure.Validation +{ + [Serializable] + public sealed class ValidationError + { + private readonly string message; + private readonly string[] propertyNames; + + public string Message + { + get { return message; } + } + + public IEnumerable PropertyNames + { + get { return propertyNames; } + } + + public ValidationError(string message, params string[] propertyNames) + { + Guard.NotNullOrEmpty(message); + + this.message = message; + + this.propertyNames = propertyNames ?? Array.Empty(); + } + + public ValidationError WithPrefix(string prefix) + { + if (propertyNames.Length > 0) + { + return new ValidationError(Message, propertyNames.Select(x => $"{prefix}.{x}").ToArray()); + } + else + { + return new ValidationError(Message, prefix); + } + } + + public void AddTo(AddValidation e) + { + e(Message, propertyNames); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs new file mode 100644 index 000000000..fb3a4bd10 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// 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 System.Runtime.Serialization; +using System.Text; + +namespace Squidex.Infrastructure.Validation +{ + [Serializable] + public class ValidationException : DomainException + { + private static readonly List FallbackErrors = new List(); + private readonly IReadOnlyList errors; + + public IReadOnlyList Errors + { + get { return errors ?? FallbackErrors; } + } + + public string Summary { get; } + + public ValidationException(string summary, params ValidationError[]? errors) + : this(summary, errors?.ToList()) + { + } + + public ValidationException(string summary, IReadOnlyList? errors) + : this(summary, null, errors) + { + } + + public ValidationException(string summary, Exception? inner, params ValidationError[]? errors) + : this(summary, inner, errors?.ToList()) + { + } + + public ValidationException(string summary, Exception? inner, IReadOnlyList? errors) + : base(FormatMessage(summary, errors), inner!) + { + Summary = summary; + + this.errors = errors ?? FallbackErrors; + } + + protected ValidationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Summary = info.GetString(nameof(Summary))!; + + errors = (List)info.GetValue(nameof(errors), typeof(List))!; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Summary), Summary); + info.AddValue(nameof(errors), errors.ToList()); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(string summary, IReadOnlyList? errors) + { + var sb = new StringBuilder(); + + sb.Append(summary.TrimEnd(' ', '.', ':')); + + if (errors?.Count > 0) + { + sb.Append(": "); + + for (var i = 0; i < errors.Count; i++) + { + var error = errors[i]?.Message; + + if (!string.IsNullOrWhiteSpace(error)) + { + sb.Append(error); + + if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) + { + sb.Append("."); + } + + if (i < errors.Count - 1) + { + sb.Append(" "); + } + } + } + } + else + { + sb.Append("."); + } + + return sb.ToString(); + } + } +} diff --git a/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs similarity index 100% rename from src/Squidex.Infrastructure/Validation/ValidationExtensions.cs rename to backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs diff --git a/src/Squidex.Infrastructure/ValueStopwatch.cs b/backend/src/Squidex.Infrastructure/ValueStopwatch.cs similarity index 100% rename from src/Squidex.Infrastructure/ValueStopwatch.cs rename to backend/src/Squidex.Infrastructure/ValueStopwatch.cs diff --git a/src/Squidex.Infrastructure/language-codes.csv b/backend/src/Squidex.Infrastructure/language-codes.csv similarity index 100% rename from src/Squidex.Infrastructure/language-codes.csv rename to backend/src/Squidex.Infrastructure/language-codes.csv diff --git a/src/Squidex.Shared/DefaultClients.cs b/backend/src/Squidex.Shared/DefaultClients.cs similarity index 100% rename from src/Squidex.Shared/DefaultClients.cs rename to backend/src/Squidex.Shared/DefaultClients.cs diff --git a/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs b/backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs similarity index 100% rename from src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs rename to backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs diff --git a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs similarity index 100% rename from src/Squidex.Shared/Identity/SquidexClaimTypes.cs rename to backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs diff --git a/backend/src/Squidex.Shared/Permissions.cs b/backend/src/Squidex.Shared/Permissions.cs new file mode 100644 index 000000000..b146e89db --- /dev/null +++ b/backend/src/Squidex.Shared/Permissions.cs @@ -0,0 +1,184 @@ +// ========================================================================== +// 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 System.Reflection; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; + +namespace Squidex.Shared +{ + public static class Permissions + { + private static readonly List ForAppsNonSchemaList = new List(); + private static readonly List ForAppsSchemaList = new List(); + + public static IReadOnlyList ForAppsNonSchema + { + get { return ForAppsNonSchemaList; } + } + + public static IReadOnlyList ForAppsSchema + { + get { return ForAppsSchemaList; } + } + + public const string All = "squidex.*"; + + public const string Admin = "squidex.admin.*"; + public const string AdminOrleans = "squidex.admin.orleans"; + + public const string AdminAppCreate = "squidex.admin.apps.create"; + + public const string AdminRestore = "squidex.admin.restore"; + + public const string AdminEvents = "squidex.admin.events"; + public const string AdminEventsRead = "squidex.admin.events.read"; + public const string AdminEventsManage = "squidex.admin.events.manage"; + + public const string AdminUsers = "squidex.admin.users"; + public const string AdminUsersRead = "squidex.admin.users.read"; + public const string AdminUsersCreate = "squidex.admin.users.create"; + public const string AdminUsersUpdate = "squidex.admin.users.update"; + public const string AdminUsersUnlock = "squidex.admin.users.unlock"; + public const string AdminUsersLock = "squidex.admin.users.lock"; + + public const string App = "squidex.apps.{app}"; + public const string AppCommon = "squidex.apps.{app}.common"; + + public const string AppDelete = "squidex.apps.{app}.delete"; + public const string AppUpdate = "squidex.apps.{app}.update"; + public const string AppUpdateImage = "squidex.apps.{app}.update"; + public const string AppUpdateGeneral = "squidex.apps.{app}.general"; + + public const string AppClients = "squidex.apps.{app}.clients"; + public const string AppClientsRead = "squidex.apps.{app}.clients.read"; + public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; + public const string AppClientsUpdate = "squidex.apps.{app}.clients.update"; + public const string AppClientsDelete = "squidex.apps.{app}.clients.delete"; + + public const string AppContributors = "squidex.apps.{app}.contributors"; + public const string AppContributorsRead = "squidex.apps.{app}.contributors.read"; + public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign"; + public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; + + public const string AppLanguages = "squidex.apps.{app}.languages"; + public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; + public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; + public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; + + public const string AppRoles = "squidex.apps.{app}.roles"; + public const string AppRolesRead = "squidex.apps.{app}.roles.read"; + public const string AppRolesCreate = "squidex.apps.{app}.roles.create"; + public const string AppRolesUpdate = "squidex.apps.{app}.roles.update"; + public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; + + public const string AppPatterns = "squidex.apps.{app}.patterns"; + public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create"; + public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; + public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; + + public const string AppWorkflows = "squidex.apps.{app}.workflows"; + public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read"; + public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create"; + public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update"; + public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete"; + + public const string AppBackups = "squidex.apps.{app}.backups"; + public const string AppBackupsRead = "squidex.apps.{app}.backups.read"; + public const string AppBackupsCreate = "squidex.apps.{app}.backups.create"; + public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete"; + + public const string AppPlans = "squidex.apps.{app}.plans"; + public const string AppPlansRead = "squidex.apps.{app}.plans.read"; + public const string AppPlansChange = "squidex.apps.{app}.plans.change"; + + public const string AppAssets = "squidex.apps.{app}.assets"; + public const string AppAssetsRead = "squidex.apps.{app}.assets.read"; + public const string AppAssetsCreate = "squidex.apps.{app}.assets.create"; + public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; + public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; + + public const string AppRules = "squidex.apps.{app}.rules"; + public const string AppRulesRead = "squidex.apps.{app}.rules.read"; + public const string AppRulesEvents = "squidex.apps.{app}.rules.events"; + public const string AppRulesCreate = "squidex.apps.{app}.rules.create"; + public const string AppRulesUpdate = "squidex.apps.{app}.rules.update"; + public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; + public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; + + public const string AppSchemas = "squidex.apps.{app}.schemas.{name}"; + public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create"; + public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; + public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; + public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; + public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete"; + + public const string AppContents = "squidex.apps.{app}.contents.{name}"; + public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; + public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; + public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; + public const string AppContentsDraftDiscard = "squidex.apps.{app}.contents.{name}.draft.discard"; + public const string AppContentsDraftPublish = "squidex.apps.{app}.contents.{name}.draft.publish"; + public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; + + public const string AppApi = "squidex.apps.{app}.api"; + + static Permissions() + { + foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field.IsLiteral && !field.IsInitOnly) + { + var value = field.GetValue(null) as string; + + if (value?.StartsWith(App, StringComparison.OrdinalIgnoreCase) == true) + { + if (value.IndexOf("{name}", App.Length, StringComparison.OrdinalIgnoreCase) >= 0) + { + ForAppsSchemaList.Add(value); + } + else + { + ForAppsNonSchemaList.Add(value); + } + } + } + } + } + + public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) + { + Guard.NotNull(id); + + return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); + } + + public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app) + { + var matching = permissions.Where(x => x.StartsWith($"squidex.apps.{app}")); + + return new PermissionSet(matching); + } + + public static string[] ToAppNames(this PermissionSet permissions) + { + var matching = permissions.Where(x => x.StartsWith("squidex.apps.")); + + var result = + matching + .Select(x => x.Id.Split('.')).Where(x => x.Length > 2) + .Select(x => x[2]) + .Distinct() + .ToArray(); + + return result; + } + } +} diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj new file mode 100644 index 000000000..57f3b5d1f --- /dev/null +++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj @@ -0,0 +1,25 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + ..\..\Squidex.ruleset + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Shared/Users/ClientUser.cs b/backend/src/Squidex.Shared/Users/ClientUser.cs new file mode 100644 index 000000000..5958dc5de --- /dev/null +++ b/backend/src/Squidex.Shared/Users/ClientUser.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; + +namespace Squidex.Shared.Users +{ + public sealed class ClientUser : IUser + { + private readonly RefToken token; + private readonly List claims; + + public ClientUser(RefToken token) + { + Guard.NotNull(token); + + this.token = token; + + claims = new List + { + new Claim(OpenIdClaims.ClientId, token.Identifier), + new Claim(SquidexClaimTypes.DisplayName, token.ToString()) + }; + } + + public string Id + { + get { return token.Identifier; } + } + + public string Email + { + get { return token.ToString(); } + } + + public bool IsLocked + { + get { return false; } + } + + public IReadOnlyList Claims + { + get { return claims; } + } + } +} diff --git a/src/Squidex.Shared/Users/IUser.cs b/backend/src/Squidex.Shared/Users/IUser.cs similarity index 100% rename from src/Squidex.Shared/Users/IUser.cs rename to backend/src/Squidex.Shared/Users/IUser.cs diff --git a/backend/src/Squidex.Shared/Users/IUserResolver.cs b/backend/src/Squidex.Shared/Users/IUserResolver.cs new file mode 100644 index 000000000..429930038 --- /dev/null +++ b/backend/src/Squidex.Shared/Users/IUserResolver.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Shared.Users +{ + public interface IUserResolver + { + Task CreateUserIfNotExists(string email, bool invited = false); + + Task FindByIdOrEmailAsync(string idOrEmail); + + Task> QueryByEmailAsync(string email); + + Task> QueryManyAsync(string[] ids); + } +} diff --git a/backend/src/Squidex.Shared/Users/UserExtensions.cs b/backend/src/Squidex.Shared/Users/UserExtensions.cs new file mode 100644 index 000000000..7a2e7c2b5 --- /dev/null +++ b/backend/src/Squidex.Shared/Users/UserExtensions.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; + +namespace Squidex.Shared.Users +{ + public static class UserExtensions + { + public static PermissionSet Permissions(this IUser user) + { + return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); + } + + public static bool IsInvited(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); + } + + public static bool IsHidden(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); + } + + public static bool HasConsent(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); + } + + public static bool HasConsentForEmails(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); + } + + public static bool HasDisplayName(this IUser user) + { + return user.HasClaim(SquidexClaimTypes.DisplayName); + } + + public static bool HasPictureUrl(this IUser user) + { + return user.HasClaim(SquidexClaimTypes.PictureUrl); + } + + public static bool IsPictureUrlStored(this IUser user) + { + return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); + } + + public static string? ClientSecret(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.ClientSecret); + } + + public static string? PictureUrl(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.PictureUrl); + } + + public static string? DisplayName(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.DisplayName); + } + + public static string? GetClaimValue(this IUser user, string type) + { + return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; + } + + public static string[] GetClaimValues(this IUser user, string type) + { + return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray(); + } + + public static bool HasClaim(this IUser user, string type) + { + return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); + } + + public static bool HasClaimValue(this IUser user, string type, string value) + { + return user.Claims.Any(x => + string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && + string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); + } + + public static string? PictureNormalizedUrl(this IUser user) + { + var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; + + if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) + { + if (url.Contains("?")) + { + url += "&d=404"; + } + else + { + url += "?d=404"; + } + } + + return url; + } + } +} diff --git a/backend/src/Squidex.Web/ApiController.cs b/backend/src/Squidex.Web/ApiController.cs new file mode 100644 index 000000000..035512bfe --- /dev/null +++ b/backend/src/Squidex.Web/ApiController.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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web +{ + [Area("Api")] + [ApiController] + [ApiExceptionFilter] + [ApiModelValidation(false)] + public abstract class ApiController : Controller + { + protected ICommandBus CommandBus { get; } + + protected IAppEntity App + { + get + { + var app = HttpContext.Context().App; + + if (app == null) + { + throw new InvalidOperationException("Not in a app context."); + } + + return app; + } + } + + protected Context Context + { + get { return HttpContext.Context(); } + } + + protected Guid AppId + { + get { return App.Id; } + } + + protected ApiController(ICommandBus commandBus) + { + Guard.NotNull(commandBus); + + CommandBus = commandBus; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + var request = context.HttpContext.Request; + + if (!request.PathBase.HasValue || !request.PathBase.Value.EndsWith("/api", StringComparison.OrdinalIgnoreCase)) + { + context.Result = new RedirectResult("/"); + } + } + } +} diff --git a/src/Squidex.Web/ApiCostsAttribute.cs b/backend/src/Squidex.Web/ApiCostsAttribute.cs similarity index 100% rename from src/Squidex.Web/ApiCostsAttribute.cs rename to backend/src/Squidex.Web/ApiCostsAttribute.cs diff --git a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs new file mode 100644 index 000000000..125e6169b --- /dev/null +++ b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// 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 System.Security; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Web +{ + public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter + { + private static readonly List> Handlers = new List>(); + + private static void AddHandler(Func handler) where T : Exception + { + Handlers.Add(ex => ex is T typed ? handler(typed) : null); + } + + static ApiExceptionFilterAttribute() + { + AddHandler(OnValidationException); + AddHandler(OnDecoderException); + AddHandler(OnDomainObjectNotFoundException); + AddHandler(OnDomainObjectVersionException); + AddHandler(OnDomainForbiddenException); + AddHandler(OnDomainException); + AddHandler(OnSecurityException); + } + + private static IActionResult OnDecoderException(DecoderFallbackException ex) + { + return ErrorResult(400, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnDomainObjectNotFoundException(DomainObjectNotFoundException ex) + { + return new NotFoundResult(); + } + + private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) + { + return ErrorResult(412, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnDomainException(DomainException ex) + { + return ErrorResult(400, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex) + { + return ErrorResult(403, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnSecurityException(SecurityException ex) + { + return ErrorResult(403, new ErrorDto { Message = ex.Message }); + } + + private static IActionResult OnValidationException(ValidationException ex) + { + return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) }); + } + + private static IActionResult ErrorResult(int statusCode, ErrorDto error) + { + error.StatusCode = statusCode; + + return new ObjectResult(error) { StatusCode = statusCode }; + } + + public void OnException(ExceptionContext context) + { + IActionResult? result = null; + + foreach (var handler in Handlers) + { + result = handler(context.Exception); + + if (result != null) + { + break; + } + } + + if (result != null) + { + context.Result = result; + } + } + + private static string[] ToDetails(ValidationException ex) + { + return ex.Errors?.Select(e => + { + if (e.PropertyNames?.Any() == true) + { + return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; + } + else + { + return e.Message; + } + }).ToArray() ?? new string[0]; + } + } +} diff --git a/src/Squidex.Web/ApiModelValidationAttribute.cs b/backend/src/Squidex.Web/ApiModelValidationAttribute.cs similarity index 100% rename from src/Squidex.Web/ApiModelValidationAttribute.cs rename to backend/src/Squidex.Web/ApiModelValidationAttribute.cs diff --git a/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs similarity index 100% rename from src/Squidex.Web/ApiPermissionAttribute.cs rename to backend/src/Squidex.Web/ApiPermissionAttribute.cs diff --git a/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs b/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs new file mode 100644 index 000000000..7a85779d4 --- /dev/null +++ b/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Assets; + +namespace Squidex.Web +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AssetRequestSizeLimitAttribute : Attribute, IAuthorizationFilter, IRequestSizePolicy + { + public void OnAuthorization(AuthorizationFilterContext context) + { + var assetOptions = context.HttpContext.RequestServices.GetService>(); + + var maxRequestBodySizeFeature = context.HttpContext.Features.Get(); + + if (maxRequestBodySizeFeature?.IsReadOnly == false) + { + if (assetOptions?.Value.MaxSize > 0) + { + maxRequestBodySizeFeature.MaxRequestBodySize = assetOptions.Value.MaxSize; + } + else + { + maxRequestBodySizeFeature.MaxRequestBodySize = null; + } + } + } + } +} diff --git a/src/Squidex.Web/ClearCookiesAttribute.cs b/backend/src/Squidex.Web/ClearCookiesAttribute.cs similarity index 100% rename from src/Squidex.Web/ClearCookiesAttribute.cs rename to backend/src/Squidex.Web/ClearCookiesAttribute.cs diff --git a/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs similarity index 100% rename from src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs rename to backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs similarity index 100% rename from src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs rename to backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs similarity index 100% rename from src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs rename to backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs new file mode 100644 index 000000000..03435941b --- /dev/null +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web.CommandMiddlewares +{ + public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware + { + private readonly IAppProvider appProvider; + private readonly IActionContextAccessor actionContextAccessor; + + public EnrichWithSchemaIdCommandMiddleware(IAppProvider appProvider, IActionContextAccessor actionContextAccessor) + { + this.appProvider = appProvider; + + this.actionContextAccessor = actionContextAccessor; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (actionContextAccessor.ActionContext == null) + { + await next(); + + return; + } + + if (context.Command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) + { + var schemaId = await GetSchemaIdAsync(context); + + schemaCommand.SchemaId = schemaId!; + } + + if (context.Command is SchemaCommand schemaSelfCommand && schemaSelfCommand.SchemaId == Guid.Empty) + { + var schemaId = await GetSchemaIdAsync(context); + + schemaSelfCommand.SchemaId = schemaId?.Id ?? Guid.Empty; + } + + await next(); + } + + private async Task?> GetSchemaIdAsync(CommandContext context) + { + NamedId? appId = null; + + if (context.Command is IAppCommand appCommand) + { + appId = appCommand.AppId; + } + + if (appId == null) + { + appId = actionContextAccessor.ActionContext.HttpContext.Context().App?.NamedId(); + } + + if (appId != null) + { + var routeValues = actionContextAccessor.ActionContext.RouteData.Values; + + if (routeValues.ContainsKey("name")) + { + var schemaName = routeValues["name"].ToString()!; + + ISchemaEntity? schema; + + if (Guid.TryParse(schemaName, out var id)) + { + schema = await appProvider.GetSchemaAsync(appId.Id, id); + } + else + { + schema = await appProvider.GetSchemaAsync(appId.Id, schemaName); + } + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaName, typeof(ISchemaEntity)); + } + + return schema.NamedId(); + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Web/Constants.cs b/backend/src/Squidex.Web/Constants.cs similarity index 100% rename from src/Squidex.Web/Constants.cs rename to backend/src/Squidex.Web/Constants.cs diff --git a/src/Squidex.Web/ContextExtensions.cs b/backend/src/Squidex.Web/ContextExtensions.cs similarity index 100% rename from src/Squidex.Web/ContextExtensions.cs rename to backend/src/Squidex.Web/ContextExtensions.cs diff --git a/backend/src/Squidex.Web/ContextProvider.cs b/backend/src/Squidex.Web/ContextProvider.cs new file mode 100644 index 000000000..e56c8ae26 --- /dev/null +++ b/backend/src/Squidex.Web/ContextProvider.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public sealed class ContextProvider : IContextProvider + { + private readonly IHttpContextAccessor httpContextAccessor; + private readonly AsyncLocal asyncLocal = new AsyncLocal(); + + public Context Context + { + get + { + if (httpContextAccessor.HttpContext == null) + { + if (asyncLocal.Value == null) + { + asyncLocal.Value = Context.Anonymous(); + } + + return asyncLocal.Value; + } + + return httpContextAccessor.HttpContext.Context(); + } + } + + public ContextProvider(IHttpContextAccessor httpContextAccessor) + { + Guard.NotNull(httpContextAccessor); + + this.httpContextAccessor = httpContextAccessor; + } + } +} diff --git a/backend/src/Squidex.Web/Deferred.cs b/backend/src/Squidex.Web/Deferred.cs new file mode 100644 index 000000000..9779689ca --- /dev/null +++ b/backend/src/Squidex.Web/Deferred.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public struct Deferred + { + private readonly Lazy> value; + + public Task Value + { + get { return value.Value; } + } + + private Deferred(Func> value) + { + this.value = new Lazy>(value); + } + + public static Deferred Response(Func factory) + { + Guard.NotNull(factory); + + return new Deferred(() => Task.FromResult(factory())); + } + + public static Deferred AsyncResponse(Func> factory) + { + Guard.NotNull(factory); + + return new Deferred(async () => (await factory())!); + } + } +} diff --git a/src/Squidex.Web/ETagExtensions.cs b/backend/src/Squidex.Web/ETagExtensions.cs similarity index 100% rename from src/Squidex.Web/ETagExtensions.cs rename to backend/src/Squidex.Web/ETagExtensions.cs diff --git a/backend/src/Squidex.Web/EntityCreatedDto.cs b/backend/src/Squidex.Web/EntityCreatedDto.cs new file mode 100644 index 000000000..4946e1af8 --- /dev/null +++ b/backend/src/Squidex.Web/EntityCreatedDto.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web +{ + public sealed class EntityCreatedDto + { + [Required] + [Display(Description = "Id of the created entity.")] + public string? Id { get; set; } + + [Display(Description = "The new version of the entity.")] + public long Version { get; set; } + + public static EntityCreatedDto FromResult(EntityCreatedResult result) + { + return new EntityCreatedDto { Id = result.IdOrValue?.ToString(), Version = result.Version }; + } + } +} diff --git a/src/Squidex.Web/ErrorDto.cs b/backend/src/Squidex.Web/ErrorDto.cs similarity index 100% rename from src/Squidex.Web/ErrorDto.cs rename to backend/src/Squidex.Web/ErrorDto.cs diff --git a/src/Squidex.Web/ExposedConfiguration.cs b/backend/src/Squidex.Web/ExposedConfiguration.cs similarity index 100% rename from src/Squidex.Web/ExposedConfiguration.cs rename to backend/src/Squidex.Web/ExposedConfiguration.cs diff --git a/backend/src/Squidex.Web/ExposedValues.cs b/backend/src/Squidex.Web/ExposedValues.cs new file mode 100644 index 000000000..d1d3c7bd4 --- /dev/null +++ b/backend/src/Squidex.Web/ExposedValues.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Configuration; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public sealed class ExposedValues : Dictionary + { + public ExposedValues() + { + } + + public ExposedValues(ExposedConfiguration configured, IConfiguration configuration, Assembly? assembly = null) + { + Guard.NotNull(configured); + Guard.NotNull(configuration); + + foreach (var kvp in configured) + { + var value = configuration.GetValue(kvp.Value); + + if (!string.IsNullOrWhiteSpace(value)) + { + this[kvp.Key] = value; + } + } + + if (assembly != null) + { + if (!ContainsKey("version")) + { + this["version"] = assembly.GetName()!.Version!.ToString(); + } + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + + foreach (var kvp in this) + { + if (sb.Length > 0) + { + sb.Append(", "); + } + + sb.Append(kvp.Key); + sb.Append(": "); + sb.Append(kvp.Value); + } + + return sb.ToString(); + } + } +} diff --git a/backend/src/Squidex.Web/Extensions.cs b/backend/src/Squidex.Web/Extensions.cs new file mode 100644 index 000000000..d8e66c748 --- /dev/null +++ b/backend/src/Squidex.Web/Extensions.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.Security; + +namespace Squidex.Web +{ + public static class Extensions + { + public static string? GetClientId(this ClaimsPrincipal principal) + { + var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; + + return clientId?.GetClientParts().ClientId; + } + + public static (string? App, string? ClientId) GetClientParts(this string clientId) + { + var parts = clientId.Split(':', '~'); + + if (parts.Length == 1) + { + return (null, parts[0]); + } + + if (parts.Length == 2) + { + return (parts[0], parts[1]); + } + + return (null, null); + } + + public static bool IsUser(this ApiController controller, string userId) + { + var subject = controller.User.OpenIdSubject(); + + return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); + } + + public static bool TryGetHeaderString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result) + { + if (headers.TryGetValue(header, out var value)) + { + string valueString = value; + + if (!string.IsNullOrWhiteSpace(valueString)) + { + result = valueString; + return true; + } + } + + result = null!; + + return false; + } + } +} diff --git a/backend/src/Squidex.Web/FileCallbackResult.cs b/backend/src/Squidex.Web/FileCallbackResult.cs new file mode 100644 index 000000000..4f1b9f4f7 --- /dev/null +++ b/backend/src/Squidex.Web/FileCallbackResult.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Web.Pipeline; + +namespace Squidex.Web +{ + public sealed class FileCallbackResult : FileResult + { + public bool Send404 { get; } + + public Func Callback { get; } + + public FileCallbackResult(string contentType, string? name, bool send404, Func callback) + : base(contentType) + { + FileDownloadName = name; + + Send404 = send404; + + Callback = callback; + } + + public override Task ExecuteResultAsync(ActionContext context) + { + var executor = context.HttpContext.RequestServices.GetRequiredService(); + + return executor.ExecuteAsync(context, this); + } + } +} + +#pragma warning restore 1573 \ No newline at end of file diff --git a/src/Squidex.Web/FileExtensions.cs b/backend/src/Squidex.Web/FileExtensions.cs similarity index 100% rename from src/Squidex.Web/FileExtensions.cs rename to backend/src/Squidex.Web/FileExtensions.cs diff --git a/src/Squidex.Web/IApiCostsFeature.cs b/backend/src/Squidex.Web/IApiCostsFeature.cs similarity index 100% rename from src/Squidex.Web/IApiCostsFeature.cs rename to backend/src/Squidex.Web/IApiCostsFeature.cs diff --git a/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs new file mode 100644 index 000000000..80e18c23e --- /dev/null +++ b/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// 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 System.Reflection; +using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; +using NJsonSchema.Converters; +using Squidex.Infrastructure; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Web.Json +{ + public class TypedJsonInheritanceConverter : JsonInheritanceConverter + { + private static readonly Lazy> DefaultMapping = new Lazy>(() => + { + var baseName = typeof(T).Name; + + var result = new Dictionary(); + + void AddType(Type type) + { + var discriminator = type.Name; + + if (discriminator.EndsWith(baseName, StringComparison.CurrentCulture)) + { + discriminator = discriminator.Substring(0, discriminator.Length - baseName.Length); + } + + result[discriminator] = type; + } + + foreach (var attribute in typeof(T).GetCustomAttributes()) + { + if (attribute.Type != null) + { + if (!attribute.Type.IsAbstract) + { + AddType(attribute.Type); + } + } + else if (!string.IsNullOrWhiteSpace(attribute.MethodName)) + { + var method = typeof(T).GetMethod(attribute.MethodName); + + if (method != null && method.IsStatic) + { + var types = (IEnumerable)method.Invoke(null, new object[0])!; + + foreach (var type in types) + { + if (!type.IsAbstract) + { + AddType(type); + } + } + } + } + } + + return result; + }); + + private readonly IReadOnlyDictionary maping; + + public TypedJsonInheritanceConverter(string discriminator) + : this(discriminator, DefaultMapping.Value) + { + } + + public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) + : base(typeof(T), discriminator) + { + maping = mapping ?? DefaultMapping.Value; + } + + protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) + { + return maping.GetOrDefault(discriminatorValue) ?? throw new InvalidOperationException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'."); + } + + public override string GetDiscriminatorValue(Type type) + { + return maping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Web/PermissionExtensions.cs b/backend/src/Squidex.Web/PermissionExtensions.cs new file mode 100644 index 000000000..59f2bf42a --- /dev/null +++ b/backend/src/Squidex.Web/PermissionExtensions.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.Security; +using AllPermissions = Squidex.Shared.Permissions; + +namespace Squidex.Web +{ + public static class PermissionExtensions + { + public static PermissionSet Permissions(this HttpContext httpContext) + { + return httpContext.Context().Permissions; + } + + public static bool Includes(this HttpContext httpContext, Permission permission, PermissionSet? additional = null) + { + return httpContext.Permissions().Includes(permission) || additional?.Includes(permission) == true; + } + + public static bool Includes(this ApiController controller, Permission permission, PermissionSet? additional = null) + { + return controller.HttpContext.Includes(permission) || additional?.Includes(permission) == true; + } + + public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet? additional = null) + { + return httpContext.Permissions().Allows(permission) || additional?.Allows(permission) == true; + } + + public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet? additional = null) + { + return controller.HttpContext.HasPermission(permission) || additional?.Allows(permission) == true; + } + + public static bool HasPermission(this ApiController controller, string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet? additional = null) + { + if (app == Permission.Any) + { + if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s) + { + app = s; + } + } + + if (schema == Permission.Any) + { + if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s) + { + schema = s; + } + } + + var permission = AllPermissions.ForApp(id, app, schema); + + return controller.HasPermission(permission, additional); + } + } +} diff --git a/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs b/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ActionContextLogAppender.cs rename to backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs diff --git a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs new file mode 100644 index 000000000..351245af0 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Web.Pipeline +{ + public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer + { + private readonly IAppPlansProvider appPlansProvider; + private readonly IUsageTracker usageTracker; + + public ApiCostsFilter(IAppPlansProvider appPlansProvider, IUsageTracker usageTracker) + { + this.appPlansProvider = appPlansProvider; + + this.usageTracker = usageTracker; + } + + IFilterMetadata IFilterContainer.FilterDefinition { get; set; } + + public ApiCostsAttribute FilterDefinition + { + get + { + return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; + } + set + { + ((IFilterContainer)this).FilterDefinition = value; + } + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + context.HttpContext.Features.Set(FilterDefinition); + + var app = context.HttpContext.Context().App; + + if (app != null && FilterDefinition.Weight > 0) + { + var appId = app.Id.ToString(); + + using (Profiler.Trace("CheckUsage")) + { + var plan = appPlansProvider.GetPlanForApp(app); + + var usage = await usageTracker.GetMonthlyCallsAsync(appId, DateTime.Today); + + if (plan?.MaxApiCalls >= 0 && usage > plan.MaxApiCalls * 1.1) + { + context.Result = new StatusCodeResult(429); + return; + } + } + + var watch = ValueStopwatch.StartNew(); + + try + { + await next(); + } + finally + { + var elapsedMs = watch.Stop(); + + await usageTracker.TrackAsync(appId, context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs); + } + } + else + { + await next(); + } + } + } +} diff --git a/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs b/backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs rename to backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs new file mode 100644 index 000000000..c15068a06 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; + +namespace Squidex.Web.Pipeline +{ + public sealed class AppResolver : IAsyncActionFilter + { + private readonly IAppProvider appProvider; + + public AppResolver(IAppProvider appProvider) + { + this.appProvider = appProvider; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var user = context.HttpContext.User; + + var appName = context.RouteData.Values["app"]?.ToString(); + + if (!string.IsNullOrWhiteSpace(appName)) + { + var app = await appProvider.GetAppAsync(appName); + + if (app == null) + { + context.Result = new NotFoundResult(); + return; + } + + var (role, permissions) = FindByOpenIdSubject(app, user); + + if (permissions == null) + { + (role, permissions) = FindByOpenIdClient(app, user); + } + + if (permissions != null) + { + var identity = user.Identities.First(); + + if (!string.IsNullOrWhiteSpace(role)) + { + identity.AddClaim(new Claim(ClaimTypes.Role, role)); + } + + foreach (var permission in permissions) + { + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); + } + } + + var requestContext = context.HttpContext.Context(); + + requestContext.App = app; + requestContext.UpdatePermissions(); + + if (!requestContext.Permissions.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context)) + { + context.Result = new NotFoundResult(); + return; + } + } + + await next(); + } + + private static bool AllowAnonymous(ActionExecutingContext context) + { + return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); + } + + private static (string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user) + { + var clientId = user.GetClientId(); + + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role)) + { + return (client.Role, role.Permissions); + } + + return (null, null); + } + + private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) + { + var subjectId = user.OpenIdSubject(); + + if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) + { + return (roleName, role.Permissions); + } + + return (null, null); + } + } +} diff --git a/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs b/backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs similarity index 100% rename from src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs rename to backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs diff --git a/src/Squidex.Web/Pipeline/DeferredActionFilter.cs b/backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs similarity index 100% rename from src/Squidex.Web/Pipeline/DeferredActionFilter.cs rename to backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs diff --git a/src/Squidex.Web/Pipeline/ETagFilter.cs b/backend/src/Squidex.Web/Pipeline/ETagFilter.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ETagFilter.cs rename to backend/src/Squidex.Web/Pipeline/ETagFilter.cs diff --git a/src/Squidex.Web/Pipeline/ETagOptions.cs b/backend/src/Squidex.Web/Pipeline/ETagOptions.cs similarity index 100% rename from src/Squidex.Web/Pipeline/ETagOptions.cs rename to backend/src/Squidex.Web/Pipeline/ETagOptions.cs diff --git a/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs b/backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs similarity index 100% rename from src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs rename to backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs diff --git a/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs similarity index 100% rename from src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs rename to backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs diff --git a/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs new file mode 100644 index 000000000..9498a5dd1 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; + +namespace Squidex.Web.Pipeline +{ + public sealed class LocalCacheMiddleware : IMiddleware + { + private readonly ILocalCache localCache; + + public LocalCacheMiddleware(ILocalCache localCache) + { + Guard.NotNull(localCache); + + this.localCache = localCache; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + using (localCache.StartContext()) + { + await next(context); + } + } + } +} diff --git a/src/Squidex.Web/Pipeline/MeasureResultFilter.cs b/backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs similarity index 100% rename from src/Squidex.Web/Pipeline/MeasureResultFilter.cs rename to backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs diff --git a/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs similarity index 100% rename from src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs rename to backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs diff --git a/backend/src/Squidex.Web/Resource.cs b/backend/src/Squidex.Web/Resource.cs new file mode 100644 index 000000000..76d9c62dd --- /dev/null +++ b/backend/src/Squidex.Web/Resource.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public abstract class Resource + { + [JsonProperty("_links")] + [Required] + [Display(Description = "The links.")] + public Dictionary Links { get; } = new Dictionary(); + + public void AddSelfLink(string href) + { + AddGetLink("self", href); + } + + public void AddGetLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "GET", href, metadata); + } + + public void AddPatchLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "PATCH", href, metadata); + } + + public void AddPostLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "POST", href, metadata); + } + + public void AddPutLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "PUT", href, metadata); + } + + public void AddDeleteLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "DELETE", href, metadata); + } + + public void AddLink(string rel, string method, string href, string? metadata = null) + { + Guard.NotNullOrEmpty(rel); + Guard.NotNullOrEmpty(href); + Guard.NotNullOrEmpty(method); + + Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; + } + } +} diff --git a/backend/src/Squidex.Web/ResourceLink.cs b/backend/src/Squidex.Web/ResourceLink.cs new file mode 100644 index 000000000..9003ecb80 --- /dev/null +++ b/backend/src/Squidex.Web/ResourceLink.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Web +{ + public class ResourceLink + { + [Required] + [Display(Description = "The link url.")] + public string Href { get; set; } + + [Required] + [Display(Description = "The link method.")] + public string Method { get; set; } + + [Display(Description = "Additional data about the link.")] + public string? Metadata { get; set; } + } +} diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs new file mode 100644 index 000000000..7cbc56f0e --- /dev/null +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Web.Services +{ + public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator, IEmailUrlGenerator + { + private readonly IAssetStore assetStore; + private readonly UrlsOptions urlsOptions; + + public bool CanGenerateAssetSourceUrl { get; } + + public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) + { + this.assetStore = assetStore; + this.urlsOptions = urlsOptions.Value; + + CanGenerateAssetSourceUrl = allowAssetSourceUrl; + } + + public string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) + { + if (!asset.IsImage) + { + return null; + } + + return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}&width=100&mode=Max"); + } + + public string GenerateUrl(string assetId) + { + return urlsOptions.BuildUrl($"api/assets/{assetId}"); + } + + public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) + { + return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}"); + } + + public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) + { + return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.SchemaDef.Name}/{content.Id}"); + } + + public string GenerateContentUIUrl(NamedId appId, NamedId schemaId, Guid contentId) + { + return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history"); + } + + public string GenerateUIUrl() + { + return urlsOptions.BuildUrl("app/"); + } + + public string? GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) + { + return assetStore.GeneratePublicUrl(asset.Id.ToString(), asset.FileVersion, null); + } + } +} diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj new file mode 100644 index 000000000..4d6a28a24 --- /dev/null +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.0 + 8.0 + enable + + + full + True + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Web/SquidexWeb.cs b/backend/src/Squidex.Web/SquidexWeb.cs similarity index 100% rename from src/Squidex.Web/SquidexWeb.cs rename to backend/src/Squidex.Web/SquidexWeb.cs diff --git a/backend/src/Squidex.Web/UrlHelperExtensions.cs b/backend/src/Squidex.Web/UrlHelperExtensions.cs new file mode 100644 index 000000000..790ecf0f8 --- /dev/null +++ b/backend/src/Squidex.Web/UrlHelperExtensions.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Web +{ + public static class UrlHelperExtensions + { + private static class NameOf + { + public static readonly string Controller; + + static NameOf() + { + const string suffix = "Controller"; + + var name = typeof(T).Name; + + if (name.EndsWith(suffix, StringComparison.Ordinal)) + { + name = name.Substring(0, name.Length - suffix.Length); + } + + Controller = name; + } + } + + public static string Url(this IUrlHelper urlHelper, Func action, object? values = null) where T : Controller + { + return urlHelper.Action(action(null), NameOf.Controller, values); + } + + public static string Url(this Controller controller, Func action, object? values = null) where T : Controller + { + return controller.Url.Url(action, values); + } + } +} diff --git a/src/Squidex.Web/UrlsOptions.cs b/backend/src/Squidex.Web/UrlsOptions.cs similarity index 100% rename from src/Squidex.Web/UrlsOptions.cs rename to backend/src/Squidex.Web/UrlsOptions.cs diff --git a/src/Squidex.Web/UsageOptions.cs b/backend/src/Squidex.Web/UsageOptions.cs similarity index 100% rename from src/Squidex.Web/UsageOptions.cs rename to backend/src/Squidex.Web/UsageOptions.cs diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs new file mode 100644 index 000000000..20a01d6b7 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Web; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class CommonProcessor : IDocumentProcessor + { + private readonly string version; + private readonly string backgroundColor = "#3f83df"; + private readonly string logoUrl; + private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation + { + Url = "https://docs.squidex.io" + }; + + public CommonProcessor(ExposedValues exposedValues, IOptions urlOptions) + { + logoUrl = urlOptions.Value.BuildUrl("images/logo-white.png", false); + + if (!exposedValues.TryGetValue("version", out version!) || version == null) + { + version = "1.0"; + } + } + + public void Process(DocumentProcessorContext context) + { + context.Document.BasePath = Constants.ApiPrefix; + + context.Document.Info.Version = version; + context.Document.Info.ExtensionData = new Dictionary + { + ["x-logo"] = new { url = logoUrl, backgroundColor } + }; + + context.Document.ExternalDocumentation = documentation; + } + } +} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs new file mode 100644 index 000000000..5dfc0dc69 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public static class OpenApiExtensions + { + public static void UseSquidexOpenApi(this IApplicationBuilder app) + { + app.UseOpenApi(); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs new file mode 100644 index 000000000..0fafa6ba0 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using NJsonSchema; +using NJsonSchema.Generation.TypeMappers; +using NodaTime; +using NSwag.Generation; +using NSwag.Generation.Processors; +using Squidex.Areas.Api.Controllers.Contents.Generator; +using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public static class OpenApiServices + { + public static void AddSquidexOpenApiSettings(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddOpenApiDocument(settings => + { + settings.ConfigureName(); + settings.ConfigureSchemaSettings(); + + settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false)); + }); + + services.AddTransient(); + } + + public static void ConfigureName(this T settings) where T : OpenApiDocumentGeneratorSettings + { + settings.Title = "Squidex API"; + } + + public static void ConfigureSchemaSettings(this T settings) where T : OpenApiDocumentGeneratorSettings + { + settings.TypeMappers = new List + { + new PrimitiveTypeMapper(typeof(Instant), schema => + { + schema.Type = JsonObjectType.String; + schema.Format = JsonFormatStrings.DateTime; + }), + + new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), + new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String), + new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String) + }; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs new file mode 100644 index 000000000..c7043f120 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Web; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class ScopesProcessor : IOperationProcessor + { + public bool Process(OperationProcessorContext context) + { + if (context.OperationDescription.Operation.Security == null) + { + context.OperationDescription.Operation.Security = new List(); + } + + var permissionAttribute = context.MethodInfo.GetCustomAttribute(); + + if (permissionAttribute != null) + { + context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement + { + [Constants.SecurityDefinition] = permissionAttribute.PermissionIds + }); + } + else + { + var authorizeAttributes = + context.MethodInfo.GetCustomAttributes(true).Union( + context.MethodInfo.DeclaringType!.GetCustomAttributes(true)) + .ToArray(); + + if (authorizeAttributes.Any()) + { + var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList(); + + context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement + { + [Constants.SecurityDefinition] = scopes + }); + } + } + + return true; + } + } +} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs diff --git a/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs similarity index 100% rename from src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs rename to backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs new file mode 100644 index 000000000..f2bf8eb99 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.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.Linq; +using System.Text.RegularExpressions; +using Namotion.Reflection; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class XmlResponseTypesProcessor : IOperationProcessor + { + private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); + + public bool Process(OperationProcessorContext context) + { + var operation = context.OperationDescription.Operation; + + var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns"); + + if (!string.IsNullOrWhiteSpace(returnsDescription)) + { + foreach (var match in ResponseRegex.Matches(returnsDescription).OfType()) + { + var statusCode = match.Groups["Code"].Value; + + if (!operation.Responses.TryGetValue(statusCode, out var response)) + { + response = new OpenApiResponse(); + + operation.Responses[statusCode] = response; + } + + var description = match.Groups["Description"].Value; + + if (description.Contains("=>")) + { + throw new InvalidOperationException("Description not formatted correcly."); + } + + response.Description = description; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs new file mode 100644 index 000000000..ce46335c1 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Namotion.Reflection; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace Squidex.Areas.Api.Config.OpenApi +{ + public sealed class XmlTagProcessor : IDocumentProcessor + { + public void Process(DocumentProcessorContext context) + { + foreach (var controllerType in context.ControllerTypes) + { + var attribute = controllerType.GetCustomAttribute(); + + if (attribute != null) + { + var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); + + if (tag != null) + { + var description = controllerType.GetXmlDocsSummary(); + + if (description != null) + { + tag.Description ??= string.Empty; + + if (!tag.Description.Contains(description)) + { + tag.Description += "\n\n" + description; + } + } + } + } + } + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs new file mode 100644 index 000000000..4ce50f255 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -0,0 +1,302 @@ +// ========================================================================== +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using NSwag.Annotations; +using Squidex.Areas.Api.Controllers.Apps.Models; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps +{ + /// + /// Manages and configures apps. + /// + [ApiExplorerSettings(GroupName = nameof(Apps))] + public sealed class AppsController : ApiController + { + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAppProvider appProvider; + private readonly IAppPlansProvider appPlansProvider; + + public AppsController(ICommandBus commandBus, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IAppProvider appProvider, + IAppPlansProvider appPlansProvider) + : base(commandBus) + { + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.appProvider = appProvider; + this.appPlansProvider = appPlansProvider; + } + + /// + /// Get your apps. + /// + /// + /// 200 => Apps returned. + /// + /// + /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all apps, where you are assigned as a contributor. + /// + [HttpGet] + [Route("apps/")] + [ProducesResponseType(typeof(AppDto[]), 200)] + [ApiPermission] + [ApiCosts(0)] + public async Task GetApps() + { + var userOrClientId = HttpContext.User.UserOrClientId()!; + var userPermissions = HttpContext.Permissions(); + + var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); + + var response = Deferred.Response(() => + { + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray(); + }); + + Response.Headers[HeaderNames.ETag] = apps.ToEtag(); + + return Ok(response); + } + + /// + /// Create a new app. + /// + /// The app object that needs to be added to squidex. + /// + /// 201 => App created. + /// 400 => App request not valid. + /// 409 => App name is already in use. + /// + /// + /// You can only create an app when you are authenticated as a user (OpenID implicit flow). + /// You will be assigned as owner of the new app automatically. + /// + [HttpPost] + [Route("apps/")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission] + [ApiCosts(0)] + public async Task PostApp([FromBody] CreateAppDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return CreatedAtAction(nameof(GetApps), response); + } + + /// + /// Update the app. + /// + /// The name of the app to update. + /// The values to update. + /// + /// 200 => App updated. + /// 404 => App not found. + /// + [HttpPut] + [Route("apps/{app}/")] + [ProducesResponseType(typeof(AppDto), 200)] + [ApiPermission(Permissions.AppUpdateGeneral)] + [ApiCosts(0)] + public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return Ok(response); + } + + /// + /// Get the app image. + /// + /// The name of the app to update. + /// The file to upload. + /// + /// 200 => App image uploaded. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission(Permissions.AppUpdateImage)] + [ApiCosts(0)] + public async Task UploadImage(string app, [OpenApiIgnore] List file) + { + var response = await InvokeCommandAsync(CreateCommand(file)); + + return Ok(response); + } + + /// + /// Get the app image. + /// + /// The name of the app. + /// + /// 200 => App image found and content or (resized) image returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(FileResult), 200)] + [AllowAnonymous] + [ApiCosts(0)] + public IActionResult GetImage(string app) + { + if (App.Image == null) + { + return NotFound(); + } + + var etag = App.Image.Etag; + + Response.Headers[HeaderNames.ETag] = etag; + + var handler = new Func(async bodyStream => + { + var assetId = App.Id.ToString(); + var assetResizedId = $"{assetId}_{etag}_Resized"; + + try + { + await assetStore.DownloadAsync(assetResizedId, bodyStream); + } + catch (AssetNotFoundException) + { + using (Profiler.Trace("Resize")) + { + using (var sourceStream = GetTempStream()) + { + using (var destinationStream = GetTempStream()) + { + using (Profiler.Trace("ResizeDownload")) + { + await assetStore.DownloadAsync(assetId, sourceStream); + sourceStream.Position = 0; + } + + using (Profiler.Trace("ResizeImage")) + { + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, 150, 150, "Crop"); + destinationStream.Position = 0; + } + + using (Profiler.Trace("ResizeUpload")) + { + await assetStore.UploadAsync(assetResizedId, destinationStream); + destinationStream.Position = 0; + } + + await destinationStream.CopyToAsync(bodyStream); + } + } + } + } + }); + + return new FileCallbackResult(App.Image.MimeType, null, true, handler); + } + + /// + /// Remove the app image. + /// + /// The name of the app to update. + /// + /// 200 => App image removed. + /// 404 => App not found. + /// + [HttpDelete] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission(Permissions.AppUpdate)] + [ApiCosts(0)] + public async Task DeleteImage(string app) + { + var response = await InvokeCommandAsync(new RemoveAppImage()); + + return Ok(response); + } + + /// + /// Archive the app. + /// + /// The name of the app to archive. + /// + /// 204 => App archived. + /// 404 => App not found. + /// + [HttpDelete] + [Route("apps/{app}/")] + [ApiPermission(Permissions.AppDelete)] + [ApiCosts(0)] + public async Task DeleteApp(string app) + { + await CommandBus.PublishAsync(new ArchiveApp()); + + return NoContent(); + } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var userOrClientId = HttpContext.User.UserOrClientId()!; + var userPermissions = HttpContext.Permissions(); + + var result = context.Result(); + var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); + + return response; + } + + private static UploadAppImage CreateCommand(IReadOnlyList file) + { + if (file.Count != 1) + { + var error = new ValidationError($"Can only upload one file, found {file.Count} files."); + + throw new ValidationException("Cannot create asset.", error); + } + + return new UploadAppImage { File = file[0].ToAssetFile() }; + } + + private static FileStream GetTempStream() + { + var tempFileName = Path.GetTempFileName(); + + return new FileStream(tempFileName, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | + FileOptions.DeleteOnClose | + FileOptions.SequentialScan); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs new file mode 100644 index 000000000..8192b050b --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -0,0 +1,244 @@ +// ========================================================================== +// 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.ComponentModel.DataAnnotations; +using NodaTime; +using Squidex.Areas.Api.Controllers.Assets; +using Squidex.Areas.Api.Controllers.Backups; +using Squidex.Areas.Api.Controllers.Ping; +using Squidex.Areas.Api.Controllers.Plans; +using Squidex.Areas.Api.Controllers.Rules; +using Squidex.Areas.Api.Controllers.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Web; +using AllPermissions = Squidex.Shared.Permissions; + +#pragma warning disable RECS0033 // Convert 'if' to '||' expression + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class AppDto : Resource + { + /// + /// The name of the app. + /// + [Required] + [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } + + /// + /// The optional label of the app. + /// + public string Label { get; set; } + + /// + /// The optional description of the app. + /// + public string Description { get; set; } + + /// + /// The version of the app. + /// + public long Version { get; set; } + + /// + /// The id of the app. + /// + public Guid Id { get; set; } + + /// + /// The timestamp when the app has been created. + /// + public Instant Created { get; set; } + + /// + /// The timestamp when the app has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The permission level of the user. + /// + public IEnumerable Permissions { get; set; } + + /// + /// Indicates if the user can access the api. + /// + public bool CanAccessApi { get; set; } + + /// + /// Indicates if the user can access at least one content. + /// + public bool CanAccessContent { get; set; } + + /// + /// Gets the current plan name. + /// + public string? PlanName { get; set; } + + /// + /// Gets the next plan name. + /// + public string? PlanUpgrade { get; set; } + + public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) + { + var permissions = GetPermissions(app, userId, userPermissions); + + var result = SimpleMapper.Map(app, new AppDto()); + + result.Permissions = permissions.ToIds(); + + if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppApi, app.Name), permissions)) + { + result.CanAccessApi = true; + } + + if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions)) + { + result.CanAccessContent = true; + } + + result.SetPlan(app, plans, controller, permissions); + result.SetImage(app, controller); + + return result.CreateLinks(controller, permissions); + } + + private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions) + { + var permissions = new List(); + + if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) + { + permissions.AddRange(role.Permissions); + } + + if (userPermissions != null) + { + permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); + } + + return new PermissionSet(permissions); + } + + private void SetPlan(IAppEntity app, IAppPlansProvider plans, ApiController controller, PermissionSet permissions) + { + if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name, additional: permissions)) + { + PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; + } + + PlanName = plans.GetPlanForApp(app).Name; + } + + private void SetImage(IAppEntity app, ApiController controller) + { + if (app.Image != null) + { + AddGetLink("image", controller.Url(x => nameof(x.GetImage), new { app = app.Name })); + } + } + + private AppDto CreateLinks(ApiController controller, PermissionSet permissions) + { + var values = new { app = Name }; + + AddGetLink("ping", controller.Url(x => nameof(x.GetAppPing), values)); + + if (controller.HasPermission(AllPermissions.AppDelete, Name, additional: permissions)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteApp), values)); + } + + if (controller.HasPermission(AllPermissions.AppUpdateGeneral, Name, additional: permissions)) + { + AddPutLink("update", controller.Url(x => nameof(x.UpdateApp), values)); + } + + if (controller.HasPermission(AllPermissions.AppUpdateImage, Name, additional: permissions)) + { + AddPostLink("image/upload", controller.Url(x => nameof(x.UploadImage), values)); + + AddDeleteLink("image/delete", controller.Url(x => nameof(x.DeleteImage), values)); + } + + if (controller.HasPermission(AllPermissions.AppAssetsRead, Name, additional: permissions)) + { + AddGetLink("assets", controller.Url(x => nameof(x.GetAssets), values)); + } + + if (controller.HasPermission(AllPermissions.AppBackupsRead, Name, additional: permissions)) + { + AddGetLink("backups", controller.Url(x => nameof(x.GetBackups), values)); + } + + if (controller.HasPermission(AllPermissions.AppClientsRead, Name, additional: permissions)) + { + AddGetLink("clients", controller.Url(x => nameof(x.GetClients), values)); + } + + if (controller.HasPermission(AllPermissions.AppContributorsRead, Name, additional: permissions)) + { + AddGetLink("contributors", controller.Url(x => nameof(x.GetContributors), values)); + } + + if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) + { + AddGetLink("languages", controller.Url(x => nameof(x.GetLanguages), values)); + } + + if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) + { + AddGetLink("patterns", controller.Url(x => nameof(x.GetPatterns), values)); + } + + if (controller.HasPermission(AllPermissions.AppPlansRead, Name, additional: permissions)) + { + AddGetLink("plans", controller.Url(x => nameof(x.GetPlans), values)); + } + + if (controller.HasPermission(AllPermissions.AppRolesRead, Name, additional: permissions)) + { + AddGetLink("roles", controller.Url(x => nameof(x.GetRoles), values)); + } + + if (controller.HasPermission(AllPermissions.AppRulesRead, Name, additional: permissions)) + { + AddGetLink("rules", controller.Url(x => nameof(x.GetRules), values)); + } + + if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) + { + AddGetLink("schemas", controller.Url(x => nameof(x.GetSchemas), values)); + } + + if (controller.HasPermission(AllPermissions.AppWorkflowsRead, Name, additional: permissions)) + { + AddGetLink("workflows", controller.Url(x => nameof(x.GetWorkflows), values)); + } + + if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, additional: permissions)) + { + AddPostLink("schemas/create", controller.Url(x => nameof(x.PostSchema), values)); + } + + if (controller.HasPermission(AllPermissions.AppAssetsCreate, Name, additional: permissions)) + { + AddPostLink("assets/create", controller.Url(x => nameof(x.PostSchema), values)); + } + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs new file mode 100644 index 000000000..cfad52a07 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Squidex.Shared; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class ContributorDto : Resource + { + private const string NotFound = "- not found -"; + + /// + /// The id of the user that contributes to the app. + /// + [Required] + public string ContributorId { get; set; } + + /// + /// The display name. + /// + [Required] + public string ContributorName { get; set; } + + /// + /// The role of the contributor. + /// + public string Role { get; set; } + + public static ContributorDto FromIdAndRole(string id, string role) + { + var result = new ContributorDto { ContributorId = id, Role = role }; + + return result; + } + + public ContributorDto WithUser(IDictionary users) + { + if (users.TryGetValue(ContributorId, out var user)) + { + ContributorName = user.DisplayName()!; + } + else + { + ContributorName = NotFound; + } + + return this; + } + + public ContributorDto WithLinks(ApiController controller, string app) + { + if (!controller.IsUser(ContributorId)) + { + if (controller.HasPermission(Permissions.AppContributorsAssign, app)) + { + AddPostLink("update", controller.Url(x => nameof(x.PostContributor), new { app })); + } + + if (controller.HasPermission(Permissions.AppContributorsRevoke, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContributor), new { app, id = ContributorId })); + } + } + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs new file mode 100644 index 000000000..c930cf146 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class UpdateWorkflowDto + { + /// + /// The name of the workflow. + /// + public string Name { get; set; } + + /// + /// The workflow steps. + /// + [Required] + public Dictionary Steps { get; set; } + + /// + /// The schema ids. + /// + public List SchemaIds { get; set; } + + /// + /// The initial step. + /// + public Status Initial { get; set; } + + public UpdateWorkflow ToCommand(Guid id) + { + var workflow = new Workflow( + Initial, + Steps?.ToDictionary( + x => x.Key, + x => x.Value?.ToStep()!), + SchemaIds, + Name); + + return new UpdateWorkflow { WorkflowId = id, Workflow = workflow }; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs new file mode 100644 index 000000000..56e51c565 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowDto : Resource + { + /// + /// The workflow id. + /// + public Guid Id { get; set; } + + /// + /// The name of the workflow. + /// + public string Name { get; set; } + + /// + /// The workflow steps. + /// + [Required] + public Dictionary Steps { get; set; } + + /// + /// The schema ids. + /// + public IReadOnlyList SchemaIds { get; set; } + + /// + /// The initial step. + /// + public Status Initial { get; set; } + + public static WorkflowDto FromWorkflow(Guid id, Workflow workflow) + { + var result = SimpleMapper.Map(workflow, new WorkflowDto + { + Steps = workflow.Steps.ToDictionary( + x => x.Key, + x => WorkflowStepDto.FromWorkflowStep(x.Value)!), + Id = id + }); + + return result; + } + + public WorkflowDto WithLinks(ApiController controller, string app) + { + var values = new { app, id = Id }; + + if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) + { + AddPutLink("update", controller.Url(x => nameof(x.PutWorkflow), values)); + } + + if (controller.HasPermission(Permissions.AppWorkflowsDelete, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteWorkflow), values)); + } + + return this; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs new file mode 100644 index 000000000..9ee0ecaf5 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowStepDto + { + /// + /// The transitions. + /// + [Required] + public Dictionary Transitions { get; set; } + + /// + /// The optional color. + /// + public string Color { get; set; } + + /// + /// Indicates if updates should not be allowed. + /// + public bool NoUpdate { get; set; } + + public static WorkflowStepDto? FromWorkflowStep(WorkflowStep step) + { + if (step == null) + { + return null; + } + + return SimpleMapper.Map(step, new WorkflowStepDto + { + Transitions = step.Transitions.ToDictionary( + y => y.Key, + y => WorkflowTransitionDto.FromWorkflowTransition(y.Value)!) + }); + } + + public WorkflowStep ToStep() + { + return new WorkflowStep( + Transitions?.ToDictionary( + y => y.Key, + y => y.Value?.ToTransition()!), + Color, NoUpdate); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs new file mode 100644 index 000000000..a76650d40 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.ObjectModel; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowTransitionDto + { + /// + /// The optional expression. + /// + public string? Expression { get; set; } + + /// + /// The optional restricted role. + /// + public ReadOnlyCollection? Roles { get; set; } + + public static WorkflowTransitionDto? FromWorkflowTransition(WorkflowTransition transition) + { + if (transition == null) + { + return null; + } + + return new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles }; + } + + public WorkflowTransition ToTransition() + { + return new WorkflowTransition(Expression, Roles); + } + } +} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs new file mode 100644 index 000000000..a7c5a4eec --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -0,0 +1,202 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Web; + +#pragma warning disable 1573 + +namespace Squidex.Areas.Api.Controllers.Assets +{ + /// + /// Uploads and retrieves assets. + /// + [ApiExplorerSettings(GroupName = nameof(Assets))] + public sealed class AssetContentController : ApiController + { + private readonly IAssetStore assetStore; + private readonly IAssetRepository assetRepository; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AssetContentController( + ICommandBus commandBus, + IAssetStore assetStore, + IAssetRepository assetRepository, + IAssetThumbnailGenerator assetThumbnailGenerator) + : base(commandBus) + { + this.assetStore = assetStore; + this.assetRepository = assetRepository; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + /// + /// Get the asset content. + /// + /// The name of the app. + /// The id or slug of the asset. + /// Optional suffix that can be used to seo-optimize the link to the image Has not effect. + /// The query string parameters. + /// + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("assets/{app}/{idOrSlug}/{*more}")] + [ProducesResponseType(typeof(FileResult), 200)] + [ApiCosts(0.5)] + [AllowAnonymous] + public async Task GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetQuery query) + { + IAssetEntity? asset; + + if (Guid.TryParse(idOrSlug, out var guid)) + { + asset = await assetRepository.FindAssetAsync(guid); + } + else + { + asset = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug); + } + + return DeliverAsset(asset, query); + } + + /// + /// Get the asset content. + /// + /// The id of the asset. + /// The query string parameters. + /// + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("assets/{id}/")] + [ProducesResponseType(typeof(FileResult), 200)] + [ApiCosts(0.5)] + public async Task GetAssetContent(Guid id, [FromQuery] AssetQuery query) + { + var asset = await assetRepository.FindAssetAsync(id); + + return DeliverAsset(asset, query); + } + + private IActionResult DeliverAsset(IAssetEntity? asset, AssetQuery query) + { + query ??= new AssetQuery(); + + if (asset == null || asset.FileVersion < query.Version) + { + return NotFound(); + } + + var fileVersion = query.Version; + + if (fileVersion <= EtagVersion.Any) + { + fileVersion = asset.FileVersion; + } + + Response.Headers[HeaderNames.ETag] = fileVersion.ToString(); + + if (query.CacheDuration > 0) + { + Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}"; + } + + var handler = new Func(async bodyStream => + { + var assetId = asset.Id.ToString(); + + if (asset.IsImage && query.ShouldResize()) + { + var assetSuffix = $"{query.Width}_{query.Height}_{query.Mode}"; + + if (query.Quality.HasValue) + { + assetSuffix += $"_{query.Quality}"; + } + + try + { + await assetStore.DownloadAsync(assetId, fileVersion, assetSuffix, bodyStream); + } + catch (AssetNotFoundException) + { + using (Profiler.Trace("Resize")) + { + using (var sourceStream = GetTempStream()) + { + using (var destinationStream = GetTempStream()) + { + using (Profiler.Trace("ResizeDownload")) + { + await assetStore.DownloadAsync(assetId, fileVersion, null, sourceStream); + sourceStream.Position = 0; + } + + using (Profiler.Trace("ResizeImage")) + { + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, query.Width, query.Height, query.Mode, query.Quality); + destinationStream.Position = 0; + } + + using (Profiler.Trace("ResizeUpload")) + { + await assetStore.UploadAsync(assetId, fileVersion, assetSuffix, destinationStream); + destinationStream.Position = 0; + } + + await destinationStream.CopyToAsync(bodyStream); + } + } + } + } + } + else + { + await assetStore.DownloadAsync(assetId, fileVersion, null, bodyStream); + } + }); + + if (query.Download == 1) + { + return new FileCallbackResult(asset.MimeType, asset.FileName, true, handler); + } + else + { + return new FileCallbackResult(asset.MimeType, null, true, handler); + } + } + + private static FileStream GetTempStream() + { + var tempFileName = Path.GetTempFileName(); + + return new FileStream(tempFileName, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | + FileOptions.DeleteOnClose | + FileOptions.SequentialScan); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs new file mode 100644 index 000000000..7a7feca6e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -0,0 +1,319 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using NSwag.Annotations; +using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Areas.Api.Controllers.Contents; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Assets +{ + /// + /// Uploads and retrieves assets. + /// + [ApiExplorerSettings(GroupName = nameof(Assets))] + public sealed class AssetsController : ApiController + { + private readonly IAssetQueryService assetQuery; + private readonly IAssetUsageTracker assetStatsRepository; + private readonly IAppPlansProvider appPlansProvider; + private readonly MyContentsControllerOptions controllerOptions; + private readonly ITagService tagService; + private readonly AssetOptions assetOptions; + + public AssetsController( + ICommandBus commandBus, + IAssetQueryService assetQuery, + IAssetUsageTracker assetStatsRepository, + IAppPlansProvider appPlansProvider, + IOptions assetOptions, + IOptions controllerOptions, + ITagService tagService) + : base(commandBus) + { + this.assetOptions = assetOptions.Value; + this.assetQuery = assetQuery; + this.assetStatsRepository = assetStatsRepository; + this.appPlansProvider = appPlansProvider; + this.controllerOptions = controllerOptions.Value; + this.tagService = tagService; + } + + /// + /// Get assets tags. + /// + /// The name of the app. + /// + /// 200 => Assets returned. + /// 404 => App not found. + /// + /// + /// Get all tags for assets. + /// + [HttpGet] + [Route("apps/{app}/assets/tags")] + [ProducesResponseType(typeof(Dictionary), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetTags(string app) + { + var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); + + Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); + + return Ok(tags); + } + + /// + /// Get assets. + /// + /// The name of the app. + /// The optional asset ids. + /// The optional json query. + /// + /// 200 => Assets returned. + /// 404 => App not found. + /// + /// + /// Get all assets for the app. + /// + [HttpGet] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetAssets(string app, [FromQuery] string? ids = null, [FromQuery] string? q = null) + { + var assets = await assetQuery.QueryAsync(Context, + Q.Empty + .WithIds(ids) + .WithJsonQuery(q) + .WithODataQuery(Request.QueryString.ToString())); + + var response = Deferred.Response(() => + { + return AssetsDto.FromAssets(assets, this, app); + }); + + if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = assets.ToEtag(); + + return Ok(response); + } + + /// + /// Get an asset by id. + /// + /// The name of the app. + /// The id of the asset to retrieve. + /// + /// 200 => Asset found. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetAsset(string app, Guid id) + { + var asset = await assetQuery.FindAssetAsync(Context, id); + + if (asset == null) + { + return NotFound(); + } + + var response = Deferred.Response(() => + { + return AssetDto.FromAsset(asset, this, app); + }); + + if (controllerOptions.EnableSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); + } + + Response.Headers[HeaderNames.ETag] = asset.ToEtag(); + + return Ok(response); + } + + /// + /// Upload a new asset. + /// + /// The name of the app. + /// The file to upload. + /// + /// 201 => Asset created. + /// 404 => App not found. + /// 400 => Asset exceeds the maximum size. + /// + /// + /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. + /// + [HttpPost] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetDto), 201)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsCreate)] + [ApiCosts(1)] + public async Task PostAsset(string app, [OpenApiIgnore] List file) + { + var assetFile = await CheckAssetFileAsync(file); + + var command = new CreateAsset { File = assetFile }; + + var response = await InvokeCommandAsync(app, command); + + return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); + } + + /// + /// Replace asset content. + /// + /// The name of the app. + /// The id of the asset. + /// The file to upload. + /// + /// 200 => Asset updated. + /// 404 => Asset or app not found. + /// 400 => Asset exceeds the maximum size. + /// + /// + /// Use multipart request to upload an asset. + /// + [HttpPut] + [Route("apps/{app}/assets/{id}/content/")] + [ProducesResponseType(typeof(AssetDto), 200)] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAssetContent(string app, Guid id, [OpenApiIgnore] List file) + { + var assetFile = await CheckAssetFileAsync(file); + + var command = new UpdateAsset { File = assetFile, AssetId = id }; + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Updates the asset. + /// + /// The name of the app. + /// The id of the asset. + /// The asset object that needs to updated. + /// + /// 200 => Asset updated. + /// 400 => Asset name not valid. + /// 404 => Asset or app not found. + /// + [HttpPut] + [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(AssetDto), 200)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Delete an asset. + /// + /// The name of the app. + /// The id of the asset to delete. + /// + /// 204 => Asset deleted. + /// 404 => Asset or app not found. + /// + [HttpDelete] + [Route("apps/{app}/assets/{id}/")] + [ApiPermission(Permissions.AppAssetsDelete)] + [ApiCosts(1)] + public async Task DeleteAsset(string app, Guid id) + { + await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); + + return NoContent(); + } + + private async Task InvokeCommandAsync(string app, ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + if (context.PlainResult is AssetCreatedResult created) + { + return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); + } + else + { + return AssetDto.FromAsset(context.Result(), this, app); + } + } + + private async Task CheckAssetFileAsync(IReadOnlyList file) + { + if (file.Count != 1) + { + var error = new ValidationError($"Can only upload one file, found {file.Count} files."); + + throw new ValidationException("Cannot create asset.", error); + } + + var formFile = file[0]; + + if (formFile.Length > assetOptions.MaxSize) + { + var error = new ValidationError($"File cannot be bigger than {assetOptions.MaxSize.ToReadableSize()}."); + + throw new ValidationException("Cannot create asset.", error); + } + + var plan = appPlansProvider.GetPlanForApp(App); + + var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); + + if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length) + { + var error = new ValidationError("You have reached your max asset size."); + + throw new ValidationException("Cannot create asset.", error); + } + + return formFile.ToAssetFile(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs new file mode 100644 index 000000000..43e621981 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Orleans; +using Squidex.Areas.Api.Controllers.Backups.Models; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Backups +{ + /// + /// Manages backups for apps. + /// + [ApiExplorerSettings(GroupName = nameof(Backups))] + public class RestoreController : ApiController + { + private readonly IGrainFactory grainFactory; + + public RestoreController(ICommandBus commandBus, IGrainFactory grainFactory) + : base(commandBus) + { + this.grainFactory = grainFactory; + } + + /// + /// Get current restore status. + /// + /// + /// 200 => Status returned. + /// + [HttpGet] + [Route("apps/restore/")] + [ProducesResponseType(typeof(RestoreJobDto), 200)] + [ApiPermission(Permissions.AdminRestore)] + public async Task GetRestoreJob() + { + var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); + + var job = await restoreGrain.GetJobAsync(); + + if (job.Value == null) + { + return NotFound(); + } + + var response = RestoreJobDto.FromJob(job.Value); + + return Ok(response); + } + + /// + /// Restore a backup. + /// + /// The backup to restore. + /// + /// 204 => Restore operation started. + /// + [HttpPost] + [Route("apps/restore/")] + [ApiPermission(Permissions.AdminRestore)] + public async Task PostRestoreJob([FromBody] RestoreRequestDto request) + { + var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); + + await restoreGrain.RestoreAsync(request.Url, User.Token()!, request.Name); + + return NoContent(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs new file mode 100644 index 000000000..9f2bb9283 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -0,0 +1,457 @@ +// ========================================================================== +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Contents.Models; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents +{ + public sealed class ContentsController : ApiController + { + private readonly MyContentsControllerOptions controllerOptions; + private readonly IContentQueryService contentQuery; + private readonly IContentWorkflow contentWorkflow; + private readonly IGraphQLService graphQl; + + public ContentsController(ICommandBus commandBus, + IContentQueryService contentQuery, + IContentWorkflow contentWorkflow, + IGraphQLService graphQl, + IOptions controllerOptions) + : base(commandBus) + { + this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; + this.controllerOptions = controllerOptions.Value; + + this.graphQl = graphQl; + } + + /// + /// GraphQL endpoint. + /// + /// The name of the app. + /// The graphql query. + /// + /// 200 => Contents retrieved or mutated. + /// 404 => Schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [HttpPost] + [Route("content/{app}/graphql/")] + [ApiPermission] + [ApiCosts(2)] + public async Task PostGraphQL(string app, [FromBody] GraphQLQuery query) + { + var (hasError, response) = await graphQl.QueryAsync(Context, query); + + if (hasError) + { + return BadRequest(response); + } + else + { + return Ok(response); + } + } + + /// + /// GraphQL endpoint (Batch). + /// + /// The name of the app. + /// The graphql queries. + /// + /// 200 => Contents retrieved or mutated. + /// 404 => Schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [HttpPost] + [Route("content/{app}/graphql/batch")] + [ApiPermission] + [ApiCosts(2)] + public async Task PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch) + { + var (hasError, response) = await graphQl.QueryAsync(Context, batch); + + if (hasError) + { + return BadRequest(response); + } + else + { + return Ok(response); + } + } + + /// + /// Queries contents. + /// + /// The name of the app. + /// The optional ids of the content to fetch. + /// + /// 200 => Contents retrieved. + /// 404 => App not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task GetAllContents(string app, [FromQuery] string ids) + { + var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); + + var response = Deferred.AsyncResponse(() => + { + return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); + }); + + if (ShouldProvideSurrogateKeys(contents)) + { + Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = contents.ToEtag(); + + return Ok(response); + } + + /// + /// Queries contents. + /// + /// The name of the app. + /// The name of the schema. + /// The optional ids of the content to fetch. + /// The optional json query. + /// + /// 200 => Contents retrieved. + /// 404 => Schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task GetContents(string app, string name, [FromQuery] string? ids = null, [FromQuery] string? q = null) + { + var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var contents = await contentQuery.QueryAsync(Context, name, + Q.Empty + .WithIds(ids) + .WithJsonQuery(q) + .WithODataQuery(Request.QueryString.ToString())); + + var response = Deferred.AsyncResponse(async () => + { + return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); + }); + + if (ShouldProvideSurrogateKeys(contents)) + { + Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); + } + + Response.Headers[HeaderNames.ETag] = contents.ToEtag(); + + return Ok(response); + } + + /// + /// Get a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// + /// 200 => Content found. + /// 404 => Content, schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task GetContent(string app, string name, Guid id) + { + var content = await contentQuery.FindContentAsync(Context, name, id); + + var response = ContentDto.FromContent(Context, content, this); + + if (controllerOptions.EnableSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); + } + + Response.Headers[HeaderNames.ETag] = content.ToEtag(); + + return Ok(response); + } + + /// + /// Get a content by version. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// The version fo the content to fetch. + /// + /// 200 => Content found. + /// 404 => Content, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/{version}/")] + [ApiPermission(Permissions.AppContentsRead)] + [ApiCosts(1)] + public async Task GetContentVersion(string app, string name, Guid id, int version) + { + var content = await contentQuery.FindContentAsync(Context, name, id, version); + + var response = ContentDto.FromContent(Context, content, this); + + if (controllerOptions.EnableSurrogateKeys) + { + Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); + } + + Response.Headers[HeaderNames.ETag] = content.ToEtag(); + + return Ok(response.Data); + } + + /// + /// Create a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The full data for the content item. + /// Indicates whether the content should be published immediately. + /// + /// 201 => Content created. + /// 404 => Content, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPost] + [Route("content/{app}/{name}/")] + [ProducesResponseType(typeof(ContentsDto), 201)] + [ApiPermission(Permissions.AppContentsCreate)] + [ApiCosts(1)] + public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; + + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(GetContent), new { app, name, id = command.ContentId }, response); + } + + /// + /// Update a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to update. + /// The full data for the content item. + /// Indicates whether the update is a proposal. + /// + /// 200 => Content updated. + /// 404 => Content, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPut] + [Route("content/{app}/{name}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission(Permissions.AppContentsUpdate)] + [ApiCosts(1)] + public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Patchs a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to patch. + /// The patch for the content item. + /// Indicates whether the patch is a proposal. + /// + /// 200 => Content patched. + /// 404 => Content, schema or app not found. + /// 400 => Content patch is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPatch] + [Route("content/{app}/{name}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission(Permissions.AppContentsUpdate)] + [ApiCosts(1)] + public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Publish a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to publish. + /// The status request. + /// + /// 200 => Content published. + /// 404 => Content, schema or app not found. + /// 400 => Request is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPut] + [Route("content/{app}/{name}/{id}/status/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission] + [ApiCosts(1)] + public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Discard changes. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to discard changes. + /// + /// 200 => Content restored. + /// 404 => Content, schema or app not found. + /// 400 => Content was not archived. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPut] + [Route("content/{app}/{name}/{id}/discard/")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermission(Permissions.AppContentsDraftDiscard)] + [ApiCosts(1)] + public async Task DiscardDraft(string app, string name, Guid id) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new DiscardChanges { ContentId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Delete a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content item to delete. + /// + /// 204 => Content deleted. + /// 404 => Content, schema or app not found. + /// + /// + /// You can create an generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpDelete] + [Route("content/{app}/{name}/{id}/")] + [ApiPermission(Permissions.AppContentsDelete)] + [ApiCosts(1)] + public async Task DeleteContent(string app, string name, Guid id) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = new DeleteContent { ContentId = id }; + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + + private async Task InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = ContentDto.FromContent(Context, result, this); + + return response; + } + + private bool ShouldProvideSurrogateKeys(IReadOnlyList response) + { + return controllerOptions.EnableSurrogateKeys && response.Count <= controllerOptions.MaxItemsForSurrogateKeys; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs new file mode 100644 index 000000000..0e415924e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -0,0 +1,194 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using NodaTime; +using Squidex.Areas.Api.Controllers.Schemas.Models; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ContentDto : Resource + { + /// + /// The if of the content item. + /// + public Guid Id { get; set; } + + /// + /// The user that has created the content item. + /// + [Required] + public RefToken CreatedBy { get; set; } + + /// + /// The user that has updated the content item. + /// + [Required] + public RefToken LastModifiedBy { get; set; } + + /// + /// The data of the content item. + /// + [Required] + public object? Data { get; set; } + + /// + /// The pending changes of the content item. + /// + public object? DataDraft { get; set; } + + /// + /// The reference data for the frontend UI. + /// + public NamedContentData? ReferenceData { get; set; } + + /// + /// Indicates if the draft data is pending. + /// + public bool IsPending { get; set; } + + /// + /// The scheduled status. + /// + public ScheduleJobDto? ScheduleJob { get; set; } + + /// + /// The date and time when the content item has been created. + /// + public Instant Created { get; set; } + + /// + /// The date and time when the content item has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The status of the content. + /// + public Status Status { get; set; } + + /// + /// The color of the status. + /// + public string StatusColor { get; set; } + + /// + /// The name of the schema. + /// + public string SchemaName { get; set; } + + /// + /// The display name of the schema. + /// + public string SchemaDisplayName { get; set; } + + /// + /// The reference fields. + /// + public FieldDto[] ReferenceFields { get; set; } + + /// + /// The version of the content. + /// + public long Version { get; set; } + + public static ContentDto FromContent(Context context, IEnrichedContentEntity content, ApiController controller) + { + var response = SimpleMapper.Map(content, new ContentDto()); + + if (context.IsFlatten()) + { + response.Data = content.Data?.ToFlatten(); + response.DataDraft = content.DataDraft?.ToFlatten(); + } + else + { + response.Data = content.Data; + response.DataDraft = content.DataDraft; + } + + if (content.ReferenceFields != null) + { + response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray(); + } + + if (content.ScheduleJob != null) + { + response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); + } + + return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name); + } + + private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema) + { + var values = new { app, name = schema, id = Id }; + + AddSelfLink(controller.Url(x => nameof(x.GetContent), values)); + + if (Version > 0) + { + var versioned = new { app, name = schema, id = Id, version = Version - 1 }; + + AddGetLink("prev", controller.Url(x => nameof(x.GetContentVersion), versioned)); + } + + if (IsPending) + { + if (controller.HasPermission(Permissions.AppContentsDraftDiscard, app, schema)) + { + AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); + } + + if (controller.HasPermission(Permissions.AppContentsDraftPublish, app, schema)) + { + AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); + } + } + + if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) + { + if (content.CanUpdate) + { + AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); + } + + if (Status == Status.Published) + { + AddPutLink("draft/propose", controller.Url(x => nameof(x.PutContent), values) + "?asDraft=true"); + } + + AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); + + if (content.Nexts != null) + { + foreach (var next in content.Nexts) + { + AddPutLink($"status/{next.Status}", controller.Url(x => nameof(x.PutContentStatus), values), next.Color); + } + } + } + + if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); + } + + return this; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs new file mode 100644 index 000000000..c6916f9c2 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ContentsDto : Resource + { + /// + /// The total number of content items. + /// + public long Total { get; set; } + + /// + /// The content items. + /// + [Required] + public ContentDto[] Items { get; set; } + + /// + /// The possible statuses. + /// + [Required] + public StatusInfoDto[] Statuses { get; set; } + + public static async Task FromContentsAsync(IResultList contents, Context context, ApiController controller, + ISchemaEntity? schema, IContentWorkflow workflow) + { + var result = new ContentsDto + { + Total = contents.Total, + Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() + }; + + if (schema != null) + { + await result.AssignStatusesAsync(workflow, schema); + + result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); + } + + return result; + } + + private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) + { + var allStatuses = await workflow.GetAllAsync(schema); + + Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); + } + + private ContentsDto CreateLinks(ApiController controller, string app, string schema) + { + var values = new { app, name = schema }; + + AddSelfLink(controller.Url(x => nameof(x.GetContents), values)); + + if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) + { + AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); + + AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); + } + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs rename to backend/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs diff --git a/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/DocsVM.cs b/backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/DocsVM.cs rename to backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs rename to backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/History/HistoryController.cs rename to backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs diff --git a/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/LanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/LanguageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/NewsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs rename to backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs diff --git a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Ping/PingController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs new file mode 100644 index 000000000..02be95cbe --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Plans.Models; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Plans +{ + /// + /// Manages and configures plans. + /// + [ApiExplorerSettings(GroupName = nameof(Plans))] + public sealed class AppPlansController : ApiController + { + private readonly IAppPlansProvider appPlansProvider; + private readonly IAppPlanBillingManager appPlansBillingManager; + + public AppPlansController(ICommandBus commandBus, + IAppPlansProvider appPlansProvider, + IAppPlanBillingManager appPlansBillingManager) + : base(commandBus) + { + this.appPlansProvider = appPlansProvider; + this.appPlansBillingManager = appPlansBillingManager; + } + + /// + /// Get app plan information. + /// + /// The name of the app. + /// + /// 200 => App plan information returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/plans/")] + [ProducesResponseType(typeof(AppPlansDto), 200)] + [ApiPermission(Permissions.AppPlansRead)] + [ApiCosts(0)] + public IActionResult GetPlans(string app) + { + var hasPortal = appPlansBillingManager.HasPortal; + + var response = Deferred.Response(() => + { + return AppPlansDto.FromApp(App, appPlansProvider, hasPortal); + }); + + Response.Headers[HeaderNames.ETag] = App.ToEtag(); + + return Ok(response); + } + + /// + /// Change the app plan. + /// + /// The name of the app. + /// Plan object that needs to be changed. + /// + /// 200 => Plan changed or redirect url returned. + /// 400 => Plan not owned by user. + /// 404 => App not found. + /// + [HttpPut] + [Route("apps/{app}/plan/")] + [ProducesResponseType(typeof(PlanChangedDto), 200)] + [ApiPermission(Permissions.AppPlansChange)] + [ApiCosts(0)] + public async Task PutPlan(string app, [FromBody] ChangePlanDto request) + { + var context = await CommandBus.PublishAsync(request.ToCommand()); + + string? redirectUri = null; + + if (context.PlainResult is RedirectToCheckoutResult result) + { + redirectUri = result.Url.ToString(); + } + + return Ok(new PlanChangedDto { RedirectUri = redirectUri }); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs new file mode 100644 index 000000000..25a24be1e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; + +namespace Squidex.Areas.Api.Controllers.Plans.Models +{ + public sealed class AppPlansDto + { + /// + /// The available plans. + /// + [Required] + public PlanDto[] Plans { get; set; } + + /// + /// The current plan id. + /// + public string? CurrentPlanId { get; set; } + + /// + /// The plan owner. + /// + public string? PlanOwner { get; set; } + + /// + /// Indicates if there is a billing portal. + /// + public bool HasPortal { get; set; } + + public static AppPlansDto FromApp(IAppEntity app, IAppPlansProvider plans, bool hasPortal) + { + var planId = app.Plan?.PlanId; + + var response = new AppPlansDto + { + CurrentPlanId = planId, + Plans = plans.GetAvailablePlans().Select(PlanDto.FromPlan).ToArray(), + PlanOwner = app.Plan?.Owner.Identifier, + HasPortal = hasPortal + }; + + return response; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs new file mode 100644 index 000000000..158a6cd96 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Areas.Api.Controllers.Plans.Models +{ + public sealed class PlanChangedDto + { + /// + /// Optional redirect uri. + /// + public string? RedirectUri { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs new file mode 100644 index 000000000..f2494c5e1 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Namotion.Reflection; +using NJsonSchema; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +namespace Squidex.Areas.Api.Controllers.Rules.Models +{ + public sealed class RuleActionProcessor : IDocumentProcessor + { + private readonly RuleRegistry ruleRegistry; + + public RuleActionProcessor(RuleRegistry ruleRegistry) + { + Guard.NotNull(ruleRegistry); + + this.ruleRegistry = ruleRegistry; + } + + public void Process(DocumentProcessorContext context) + { + try + { + var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false); + + if (schema != null) + { + schema.DiscriminatorObject = new OpenApiDiscriminator + { + JsonInheritanceConverter = new RuleActionConverter(), PropertyName = "actionType" + }; + + schema.Properties["actionType"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, IsRequired = true + }; + + foreach (var (key, value) in ruleRegistry.Actions) + { + var derivedSchema = context.SchemaGenerator.Generate(value.Type.ToContextualType(), context.SchemaResolver); + + var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; + + if (oldName != null) + { + context.Document.Definitions.Remove(oldName); + context.Document.Definitions.Add($"{key}RuleActionDto", derivedSchema); + } + } + + RemoveFreezable(context, schema); + } + } + catch (KeyNotFoundException) + { + return; + } + } + + private static void RemoveFreezable(DocumentProcessorContext context, JsonSchema schema) + { + context.Document.Definitions.Remove("Freezable"); + + schema.AllOf.Clear(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs new file mode 100644 index 000000000..0c127730b --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Web.Json; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + [JsonConverter(typeof(TypedJsonInheritanceConverter), "fieldType")] + [KnownType(nameof(Subtypes))] + public abstract class FieldPropertiesDto + { + /// + /// Optional label for the editor. + /// + [StringLength(100)] + public string? Label { get; set; } + + /// + /// Hints to describe the schema. + /// + [StringLength(1000)] + public string? Hints { get; set; } + + /// + /// Placeholder to show when no value has been entered. + /// + [StringLength(100)] + public string? Placeholder { get; set; } + + /// + /// Indicates if the field is required. + /// + public bool IsRequired { get; set; } + + /// + /// Determines if the field should be displayed in lists. + /// + public bool IsListField { get; set; } + + /// + /// Determines if the field should be displayed in reference lists. + /// + public bool IsReferenceField { get; set; } + + /// + /// Optional url to the editor. + /// + public string? EditorUrl { get; set; } + + /// + /// Tags for automation processes. + /// + public ReadOnlyCollection Tags { get; set; } + + public abstract FieldProperties ToProperties(); + + public static Type[] Subtypes() + { + var type = typeof(FieldPropertiesDto); + + return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs new file mode 100644 index 000000000..173b9271a --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Entities.Schemas.Commands; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + public sealed class UpdateFieldDto + { + /// + /// The field properties. + /// + [Required] + public FieldPropertiesDto Properties { get; set; } + + public UpdateField ToCommand(long id, long? parentId = null) + { + return new UpdateField { ParentFieldId = parentId, FieldId = id, Properties = Properties?.ToProperties()! }; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs new file mode 100644 index 000000000..be7290131 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + public abstract class UpsertSchemaDto + { + /// + /// The optional properties. + /// + public SchemaPropertiesDto? Properties { get; set; } + + /// + /// The optional scripts. + /// + public SchemaScriptsDto? Scripts { get; set; } + + /// + /// Optional fields. + /// + public List Fields { get; set; } + + /// + /// The optional preview urls. + /// + public Dictionary? PreviewUrls { get; set; } + + /// + /// The category. + /// + public string Category { get; set; } + + /// + /// Set it to true to autopublish the schema. + /// + public bool IsPublished { get; set; } + + public static TCommand ToCommand(TDto dto, TCommand command) where TCommand : UpsertCommand where TDto : UpsertSchemaDto + { + SimpleMapper.Map(dto, command); + + if (dto.Properties != null) + { + command.Properties = new SchemaProperties(); + + SimpleMapper.Map(dto.Properties, command.Properties); + } + + if (dto.Scripts != null) + { + command.Scripts = new SchemaScripts(); + + SimpleMapper.Map(dto.Scripts, command.Scripts); + } + + if (dto.Fields != null) + { + command.Fields = new List(); + + foreach (var rootFieldDto in dto.Fields) + { + var rootProperties = rootFieldDto?.Properties?.ToProperties(); + var rootField = new UpsertSchemaField { Properties = rootProperties! }; + + if (rootFieldDto != null) + { + SimpleMapper.Map(rootFieldDto, rootField); + + if (rootFieldDto?.Nested?.Count > 0) + { + rootField.Nested = new List(); + + foreach (var nestedFieldDto in rootFieldDto.Nested) + { + var nestedProperties = nestedFieldDto?.Properties?.ToProperties(); + var nestedField = new UpsertSchemaNestedField { Properties = nestedProperties! }; + + if (nestedFieldDto != null) + { + SimpleMapper.Map(nestedFieldDto, nestedField); + } + + rootField.Nested.Add(nestedField); + } + } + } + + command.Fields.Add(rootField); + } + } + + return command; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs new file mode 100644 index 000000000..14154d376 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -0,0 +1,330 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Schemas.Models; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Schemas +{ + /// + /// Manages and retrieves information about schemas. + /// + [ApiExplorerSettings(GroupName = nameof(Schemas))] + public sealed class SchemasController : ApiController + { + private readonly IAppProvider appProvider; + + public SchemasController(ICommandBus commandBus, IAppProvider appProvider) + : base(commandBus) + { + this.appProvider = appProvider; + } + + /// + /// Get schemas. + /// + /// The name of the app. + /// + /// 200 => Schemas returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/schemas/")] + [ProducesResponseType(typeof(SchemasDto), 200)] + [ApiPermission(Permissions.AppCommon)] + [ApiCosts(0)] + public async Task GetSchemas(string app) + { + var schemas = await appProvider.GetSchemasAsync(AppId); + + var response = Deferred.Response(() => + { + return SchemasDto.FromSchemas(schemas, this, app); + }); + + Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); + + return Ok(response); + } + + /// + /// Get a schema by name. + /// + /// The name of the app. + /// The name of the schema to retrieve. + /// + /// 200 => Schema found. + /// 404 => Schema or app not found. + /// + [HttpGet] + [Route("apps/{app}/schemas/{name}/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppCommon)] + [ApiCosts(0)] + public async Task GetSchema(string app, string name) + { + ISchemaEntity? schema; + + if (Guid.TryParse(name, out var id)) + { + schema = await appProvider.GetSchemaAsync(AppId, id); + } + else + { + schema = await appProvider.GetSchemaAsync(AppId, name); + } + + if (schema == null || schema.IsDeleted) + { + return NotFound(); + } + + var response = Deferred.Response(() => + { + return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); + }); + + Response.Headers[HeaderNames.ETag] = schema.ToEtag(); + + return Ok(response); + } + + /// + /// Create a new schema. + /// + /// The name of the app. + /// The schema object that needs to be added to the app. + /// + /// 201 => Schema created. + /// 400 => Schema name or properties are not valid. + /// 409 => Schema name already in use. + /// + [HttpPost] + [Route("apps/{app}/schemas/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 201)] + [ApiPermission(Permissions.AppSchemasCreate)] + [ApiCosts(1)] + public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return CreatedAtAction(nameof(GetSchema), new { app, name = request.Name }, response); + } + + /// + /// Update a schema. + /// + /// The name of the app. + /// The name of the schema. + /// The schema object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Synchronize a schema. + /// + /// The name of the app. + /// The name of the schema. + /// The schema object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/sync")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Update a schema category. + /// + /// The name of the app. + /// The name of the schema. + /// The schema object that needs to updated. + /// + /// 200 => Schema updated. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/category")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Update the preview urls. + /// + /// The name of the app. + /// The name of the schema. + /// The preview urls for the schema. + /// + /// 200 => Schema updated. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/preview-urls")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Update the scripts. + /// + /// The name of the app. + /// The name of the schema. + /// The schema scripts object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/scripts/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasScripts)] + [ApiCosts(1)] + public async Task PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Publish a schema. + /// + /// The name of the app. + /// The name of the schema to publish. + /// + /// 200 => Schema has been published. + /// 400 => Schema is already published. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/publish/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasPublish)] + [ApiCosts(1)] + public async Task PublishSchema(string app, string name) + { + var command = new PublishSchema(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Unpublish a schema. + /// + /// The name of the app. + /// The name of the schema to unpublish. + /// + /// 200 => Schema has been unpublished. + /// 400 => Schema is not published. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/unpublish/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermission(Permissions.AppSchemasPublish)] + [ApiCosts(1)] + public async Task UnpublishSchema(string app, string name) + { + var command = new UnpublishSchema(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Delete a schema. + /// + /// The name of the app. + /// The name of the schema to delete. + /// + /// 204 => Schema deleted. + /// 404 => Schema or app not found. + /// + [HttpDelete] + [Route("apps/{app}/schemas/{name}/")] + [ApiPermission(Permissions.AppSchemasDelete)] + [ApiCosts(1)] + public async Task DeleteSchema(string app, string name) + { + await CommandBus.PublishAsync(new DeleteSchema()); + + return NoContent(); + } + + private async Task InvokeCommandAsync(string app, ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app); + + return response; + } + } +} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs rename to backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/UI/UIController.cs rename to backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs diff --git a/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png b/backend/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png rename to backend/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs new file mode 100644 index 000000000..dbc08c87d --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared.Users; +using Squidex.Web; +using AllPermissions = Squidex.Shared.Permissions; + +namespace Squidex.Areas.Api.Controllers.Users.Models +{ + public sealed class UserDto : Resource + { + /// + /// The id of the user. + /// + [Required] + public string Id { get; set; } + + /// + /// The email of the user. Unique value. + /// + [Required] + public string Email { get; set; } + + /// + /// The display name (usually first name and last name) of the user. + /// + [Required] + public string DisplayName { get; set; } + + /// + /// Determines if the user is locked. + /// + [Required] + public bool IsLocked { get; set; } + + /// + /// Additional permissions for the user. + /// + [Required] + public IEnumerable Permissions { get; set; } + + public static UserDto FromUser(IUser user, ApiController controller) + { + var userPermssions = user.Permissions().ToIds(); + var userName = user.DisplayName()!; + + var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); + + return result.CreateLinks(controller); + } + + private UserDto CreateLinks(ApiController controller) + { + var values = new { id = Id }; + + if (controller is UserManagementController) + { + AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); + } + else + { + AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); + } + + if (!controller.IsUser(Id)) + { + if (controller.HasPermission(AllPermissions.AdminUsersLock) && !IsLocked) + { + AddPutLink("lock", controller.Url(c => nameof(c.LockUser), values)); + } + + if (controller.HasPermission(AllPermissions.AdminUsersUnlock) && IsLocked) + { + AddPutLink("unlock", controller.Url(c => nameof(c.UnlockUser), values)); + } + } + + if (controller.HasPermission(AllPermissions.AdminUsersUpdate)) + { + AddPutLink("update", controller.Url(c => nameof(c.PutUser), values)); + } + + AddGetLink("picture", controller.Url(c => nameof(c.GetUserPicture), values)); + + return this; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs similarity index 100% rename from src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs new file mode 100644 index 000000000..20638bb74 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Squidex.Areas.Api.Controllers.Users.Models; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Users +{ + [ApiModelValidation(true)] + public sealed class UserManagementController : ApiController + { + private readonly UserManager userManager; + private readonly IUserFactory userFactory; + + public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) + : base(commandBus) + { + this.userManager = userManager; + this.userFactory = userFactory; + } + + [HttpGet] + [Route("user-management/")] + [ProducesResponseType(typeof(UsersDto), 200)] + [ApiPermission(Permissions.AdminUsersRead)] + public async Task GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) + { + var taskForItems = userManager.QueryByEmailAsync(query, take, skip); + var taskForCount = userManager.CountByEmailAsync(query); + + await Task.WhenAll(taskForItems, taskForCount); + + var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this); + + return Ok(response); + } + + [HttpGet] + [Route("user-management/{id}/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersRead)] + public async Task GetUser(string id) + { + var user = await userManager.FindByIdWithClaimsAsync(id); + + if (user == null) + { + return NotFound(); + } + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPost] + [Route("user-management/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersCreate)] + public async Task PostUser([FromBody] CreateUserDto request) + { + var user = await userManager.CreateAsync(userFactory, request.ToValues()); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPut] + [Route("user-management/{id}/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersUpdate)] + public async Task PutUser(string id, [FromBody] UpdateUserDto request) + { + var user = await userManager.UpdateAsync(id, request.ToValues()); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPut] + [Route("user-management/{id}/lock/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersLock)] + public async Task LockUser(string id) + { + if (this.IsUser(id)) + { + throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself.")); + } + + var user = await userManager.LockAsync(id); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + + [HttpPut] + [Route("user-management/{id}/unlock/")] + [ProducesResponseType(typeof(UserDto), 201)] + [ApiPermission(Permissions.AdminUsersUnlock)] + public async Task UnlockUser(string id) + { + if (this.IsUser(id)) + { + throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself.")); + } + + var user = await userManager.UnlockAsync(id); + + var response = UserDto.FromUser(user, this); + + return Ok(response); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs new file mode 100644 index 000000000..64ab97444 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Squidex.Areas.Api.Controllers.Users.Models; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Users +{ + /// + /// Readonly API to retrieve information about squidex users. + /// + [ApiExplorerSettings(GroupName = nameof(Users))] + public sealed class UsersController : ApiController + { + private static readonly byte[] AvatarBytes; + private readonly IUserPictureStore userPictureStore; + private readonly IUserResolver userResolver; + private readonly ISemanticLog log; + + static UsersController() + { + var assembly = typeof(UsersController).Assembly; + + using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) + { + AvatarBytes = new byte[avatarStream!.Length]; + + avatarStream.Read(AvatarBytes, 0, AvatarBytes.Length); + } + } + + public UsersController( + ICommandBus commandBus, + IUserPictureStore userPictureStore, + IUserResolver userResolver, + ISemanticLog log) + : base(commandBus) + { + this.userPictureStore = userPictureStore; + this.userResolver = userResolver; + + this.log = log; + } + + /// + /// Get the user resources. + /// + /// + /// 200 => User resources returned. + /// + [HttpGet] + [Route("/")] + [ProducesResponseType(typeof(ResourcesDto), 200)] + [ApiPermission] + public IActionResult GetUserResources() + { + var response = ResourcesDto.FromController(this); + + return Ok(response); + } + + /// + /// Get users by query. + /// + /// The query to search the user by email address. Case invariant. + /// + /// Search the user by query that contains the email address or the part of the email address. + /// + /// + /// 200 => Users returned. + /// + [HttpGet] + [Route("users/")] + [ProducesResponseType(typeof(UserDto[]), 200)] + [ApiPermission] + public async Task GetUsers(string query) + { + try + { + var users = await userResolver.QueryByEmailAsync(query); + + var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray(); + + return Ok(response); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", nameof(GetUsers)) + .WriteProperty("status", "Failed")); + } + + return Ok(new UserDto[0]); + } + + /// + /// Get user by id. + /// + /// The id of the user (GUID). + /// + /// 200 => User found. + /// 404 => User not found. + /// + [HttpGet] + [Route("users/{id}/")] + [ProducesResponseType(typeof(UserDto), 200)] + [ApiPermission] + public async Task GetUser(string id) + { + try + { + var entity = await userResolver.FindByIdOrEmailAsync(id); + + if (entity != null) + { + var response = UserDto.FromUser(entity, this); + + return Ok(response); + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", nameof(GetUser)) + .WriteProperty("status", "Failed")); + } + + return NotFound(); + } + + /// + /// Get user picture by id. + /// + /// The id of the user (GUID). + /// + /// 200 => User found and image or fallback returned. + /// 404 => User not found. + /// + [HttpGet] + [Route("users/{id}/picture/")] + [ProducesResponseType(typeof(FileResult), 200)] + [ResponseCache(Duration = 300)] + public async Task GetUserPicture(string id) + { + try + { + var entity = await userResolver.FindByIdOrEmailAsync(id); + + if (entity != null) + { + if (entity.IsPictureUrlStored()) + { + return new FileStreamResult(await userPictureStore.DownloadAsync(entity.Id), "image/png"); + } + + using (var client = new HttpClient()) + { + var url = entity.PictureNormalizedUrl(); + + if (!string.IsNullOrWhiteSpace(url)) + { + var response = await client.GetAsync(url); + + if (response.IsSuccessStatusCode) + { + var contentType = response.Content.Headers.ContentType.ToString(); + + return new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType); + } + } + } + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", nameof(GetUser)) + .WriteProperty("status", "Failed")); + } + + return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Startup.cs b/backend/src/Squidex/Areas/Api/Startup.cs new file mode 100644 index 000000000..e8fab3e7b --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Startup.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Squidex.Areas.Api.Config.OpenApi; +using Squidex.Web; + +namespace Squidex.Areas.Api +{ + public static class Startup + { + public static void ConfigureApi(this IApplicationBuilder app) + { + app.Map(Constants.ApiPrefix, appApi => + { + appApi.UseRouting(); + + appApi.UseAuthentication(); + appApi.UseAuthorization(); + + appApi.UseSquidexOpenApi(); + + appApi.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + }); + } + } +} diff --git a/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml b/backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml similarity index 100% rename from src/Squidex/Areas/Api/Views/Shared/Docs.cshtml rename to backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml diff --git a/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs similarity index 100% rename from src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs rename to backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs diff --git a/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs similarity index 100% rename from src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs rename to backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs new file mode 100644 index 000000000..60d8d7add --- /dev/null +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Squidex.Areas.Frontend.Middlewares +{ + public sealed class WebpackMiddleware + { + private const string WebpackUrl = "http://localhost:3000/index.html"; + private readonly RequestDelegate next; + + public WebpackMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task Invoke(HttpContext context) + { + if (context.IsIndex() && context.Response.StatusCode != 304) + { + using (var client = new HttpClient()) + { + var result = await client.GetAsync(WebpackUrl); + + context.Response.StatusCode = (int)result.StatusCode; + + if (result.IsSuccessStatusCode) + { + var html = await result.Content.ReadAsStringAsync(); + + html = html.AdjustHtml(context); + + await context.Response.WriteAsync(html); + } + } + } + else if (context.IsHtmlPath() && context.Response.StatusCode != 304) + { + var responseBuffer = new MemoryStream(); + var responseBody = context.Response.Body; + + context.Response.Body = responseBuffer; + + await next(context); + + if (context.Response.StatusCode != 304) + { + context.Response.Body = responseBody; + + var html = Encoding.UTF8.GetString(responseBuffer.ToArray()); + + html = html.AdjustHtml(context); + + context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); + context.Response.Body = responseBody; + + await context.Response.WriteAsync(html); + } + } + else + { + await next(context); + } + } + } +} diff --git a/backend/src/Squidex/Areas/Frontend/Startup.cs b/backend/src/Squidex/Areas/Frontend/Startup.cs new file mode 100644 index 000000000..9df94bfe1 --- /dev/null +++ b/backend/src/Squidex/Areas/Frontend/Startup.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Frontend.Middlewares; +using Squidex.Pipeline.Squid; + +namespace Squidex.Areas.Frontend +{ + public static class Startup + { + public static void ConfigureFrontend(this IApplicationBuilder app) + { + var environment = app.ApplicationServices.GetRequiredService(); + + app.UseMiddleware(); + + app.Use((context, next) => + { + if (context.Request.Path == "/client-callback-popup") + { + context.Request.Path = new PathString("/client-callback-popup.html"); + } + else if (context.Request.Path == "/client-callback-silent") + { + context.Request.Path = new PathString("/client-callback-silent.html"); + } + else if (!Path.HasExtension(context.Request.Path.Value)) + { + if (environment.IsDevelopment()) + { + context.Request.Path = new PathString("/index.html"); + } + else + { + context.Request.Path = new PathString("/build/index.html"); + } + } + + return next(); + }); + + if (environment.IsDevelopment()) + { + app.UseMiddleware(); + } + else + { + app.UseMiddleware(); + } + + app.UseStaticFiles(new StaticFileOptions + { + OnPrepareResponse = context => + { + var response = context.Context.Response; + var responseHeaders = response.GetTypedHeaders(); + + if (!string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) + { + responseHeaders.CacheControl = new CacheControlHeaderValue + { + MaxAge = TimeSpan.FromDays(60) + }; + } + else + { + responseHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true + }; + } + } + }); + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt new file mode 100644 index 000000000..1f58f13a9 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIULb5or4cjujSTMg9dZOFJtUU0h2MwDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UEAwwKc3F1aWRleC5pbzAeFw0xOTEwMjUxODAwMzJaFw0yOTEw +MjIxODAwMzJaMBUxEzARBgNVBAMMCnNxdWlkZXguaW8wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDkCBwdntYhthsvwj7TobnKplejrvZvkMT79SGx4tXe +5eFDpuMGqljMUzZoUaaQsy4mRe/4c9bMmMlpRmIPpJfYAYDxGzSm9N0FjUZqGJPq +kRNTNKqTPeFwl7vdn+MuveLWyOc+mSYWkPWg9WURdN5yNtDi1IUrcj+XsUqH0AQi +eEiiQJlSUF8NkdvviGv906uOG/59NwpvCsyHwV3dOR2GFHvi2RIg/M+4d8Vz2AVm +KvXVoQtxySweXRXNKvoAeKdvQ4prHc0oHo57vhW/nDV++WUGZ7LOnpr4OgIyrvM/ +0LFfSIdgJ2rK/+yKNQLJKZbq0DD942PvQapEC9Xfuuqw1S6wHXgf3CzPuoNX4sDI +2rIw84AjH0gB7IuwC0DVqcbI++Xv6H3HzpD0i1ONaHztmp5tCx9v6dkJ0ctX6vzC +SwMxdUerB9XELifOF9CqnSjUzOiAuQ9yTE0iqb6jRu5JTGeKTtmFQ6T0YUuqiPip +H3zblloGKo3mQbVumvfELb0wTs3Ay0jaczjD0aM3fRKav+6b6Qu+tDnlxL/yPde2 +SMxDwIKIH8eCAa3+8OU9VSZ0+2DS/pu1vXdsXqa5EXJJ7Ej/NyQe+nKjOG/TfeYG +u+GAO5/pJYE4lWq77hZd9ylm21dbs+X4X3uZFOsGb0BCoQADi/QFwexcz+hOmQuX +BwIDAQABo1MwUTAdBgNVHQ4EFgQUuLxsPj+ueDfckXVeG6aRFM0KVc0wHwYDVR0j +BBgwFoAUuLxsPj+ueDfckXVeG6aRFM0KVc0wDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEA0ePG4Xp29yWkDHO2Zp6dG/uVy15sEVpG25VlwEuXzdnb +VDN1++spoTLZT0HWGs9OZtCkXF2wfFL3C/az5sSn40uXy5UzoHtldEjTchSFX2tN +ulVbSiEUyxp2xEzbELIPaELhecPUyJMKUTHOLfrLaKWFC26KQK+R5E4mdx0nIZ73 +9GZFDA7okfzqkl3CeLhHfkKrPy/dLcz9doBkca4scSmgJcMQvS2sC7wVrcTtfcsh +cefQ8hMR4vfVQHl0mU7cHUJR1U7sSrXh/pOjrzX/0k/VGO+pQtDVnT8YZXRx+w0S +4nRz60nUxIDbad/xld71YV6L3rWYy2/7MIbCb71mszc6SdQtV/+lc3yJJdvNmNtc +xlpirsI1vr3yfPcuYuS8i0dqPlh7Rn+wlrqFNlu6pgpB5uhVCHXfkf3TATGJpyi3 +lN/f98Du6ZDvsIFk6loWJ/SkRAgX4un3mVEeonDMSaWAHwfPdoMXE5ViCgKLPo+B +HHM5bmZmUk25mgFoiRYx/jnw2Ym+Vsyw6SI0+kQLLoAfP/pP39rWe+MbSIhhDXC6 +5lP5IebfzEI10PAg9UrgSDShAT2E4fFaMx0mRi0dwhRgBJEW/EEjjXd8+QjXPuPF +GqU6YTf/rcDQB4cT/GaBkUar3qanmBESAabMoabZ0EDVprwrrfbqx9bDsOz4J9k= +-----END CERTIFICATE----- diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key new file mode 100644 index 000000000..2fd9618e8 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDkCBwdntYhthsv +wj7TobnKplejrvZvkMT79SGx4tXe5eFDpuMGqljMUzZoUaaQsy4mRe/4c9bMmMlp +RmIPpJfYAYDxGzSm9N0FjUZqGJPqkRNTNKqTPeFwl7vdn+MuveLWyOc+mSYWkPWg +9WURdN5yNtDi1IUrcj+XsUqH0AQieEiiQJlSUF8NkdvviGv906uOG/59NwpvCsyH +wV3dOR2GFHvi2RIg/M+4d8Vz2AVmKvXVoQtxySweXRXNKvoAeKdvQ4prHc0oHo57 +vhW/nDV++WUGZ7LOnpr4OgIyrvM/0LFfSIdgJ2rK/+yKNQLJKZbq0DD942PvQapE +C9Xfuuqw1S6wHXgf3CzPuoNX4sDI2rIw84AjH0gB7IuwC0DVqcbI++Xv6H3HzpD0 +i1ONaHztmp5tCx9v6dkJ0ctX6vzCSwMxdUerB9XELifOF9CqnSjUzOiAuQ9yTE0i +qb6jRu5JTGeKTtmFQ6T0YUuqiPipH3zblloGKo3mQbVumvfELb0wTs3Ay0jaczjD +0aM3fRKav+6b6Qu+tDnlxL/yPde2SMxDwIKIH8eCAa3+8OU9VSZ0+2DS/pu1vXds +Xqa5EXJJ7Ej/NyQe+nKjOG/TfeYGu+GAO5/pJYE4lWq77hZd9ylm21dbs+X4X3uZ +FOsGb0BCoQADi/QFwexcz+hOmQuXBwIDAQABAoICAEQsLIOqfegcMmqHzxKkMhBk +xKS55REbndiZw5YT886suTjphsvyV5PWeNidOIfgGbb1h7WmpBwMvYJMuXplwcOh +R3RNpuMXJ5DGWLvVVzt0XeutPiXBBUoNAuxSJbBOsqd17rRnQtzSP6z8UFf0saBB +xRdbY+jGQj7OkTKjPOk1PrnLSEs0ngZHihJFncuH4a0dr2qt7t+dweIALFi7/5ib +PSJntSTJkCxdGln0xkByLYbNm8dL1nXJbIAnDhDgAWahMZuukCwjXoOeI5BiWhf4 +5XwRuoJNJpV5eji+1xhIAw8yds6HWkUQWB5FlOyhE25mCY+N0M2xuv6W7zzw+8KL +gGeF44++Lo4cH7mimfsqofCMeky6NaRSwziEBvwvOf4PEBVeB4t5BhWCTWuhrmBi +fShFRY1EcdOe2nTnDV9kDKBAta7YKA1c37++OtQPdcMy4qAIUe7Yk5h3GkcnBx5+ ++wwTa+QJAX36iT9+zoxfq02mHTbPD0fkqSmdw4PM+6jkM6xJqXeSU9U4nPerIYxJ +3Pc2LBAhQ52JQNAHegEVMoL3hFElVohnXCsgAQC4mV5/f8hBfI2gqNZqDjBBn09a +NfDTMMfL2kCRBdfHtl+FELQ7vYuQ/R8OvU3M48NtFTcbdUfXn9zbGUhugyUr6E62 +6/Ck8k87EXn1nwwpxwpBAoIBAQD+buYwZsvbABjs3kYVNcNC6sWxYAbt9O9gSTw2 +apcKdHEFBCFVjerDl1kFQGLcrAcHBYkcViCpWrIGQiSMQe90Evsgkl61Txtm65KY +fjjiarYuCPfNU3cMhs7xnGSK7ydGgOHLbjUpOgrJQxVMXQpLhp0DrOGZ0txWuPYO +bDhb3OqZawEQqcc8GYA32YU/qUK7HitqrLlNo+CQIO4UFbZyh9f99pHayWfa0H/V +SJ7iUwZKNjRbwd9n0RoxmhADvr9Tca3XFvFsjpFxmuR4a141SVqxy/52ciPA5/e8 +ptRhoEx+jK2pEK16NC7+ut3QS4DFh1T0VkwODE+ycZDwVmNnAoIBAQDlb5cLK58c +VbKtvXQh7Xaa1cNRI97UHuS1HAj9BWIysITRoRyxhn5Y0HgY2E4i5ZVIR0IAZskk +x5A3vyPNSi1AA5XCgHB6UCDoMvXr+HSnB05eMwbnIrQL4mEC37/Y9cwI/ZM5D8fG +aP3zPllDet/8F2ohEfkrggvCztCnPKxO5cZRYpXz9S0dVnyF4tTS8qJ9L9vQqBTk +gV2jIL5p4n//Z2wcw1Oaigc1KjaM+5yPJs34Yo7WAaJrxvjBIWD4snG2eH+MYEpN +1ATghut5KZuIVEPzMaxuUk4FdsckunCoCqK/nfbmIot+b35eG7pODxOvnR2JRTb5 +NOaYd041kYthAoIBAQDmVftqIgW3E1V9SpRjqzJEKEokk+xyC+WRY3txP/nQ6y1N +/zk2PK4lt6RNjsZxRANwpeBEmOwkpQi5hbOUjjR6/pv+FsRKm30RJX6nMs3InBal +glTjuwXxfzFlpdGXvX3u48qF4hWaZwNQxLxJT4l8ajdHFoF+Qlha4kNPN0WmVE7F +6QsjzK+jhup+pRtuUIsq3tsrTYbL9OndURJ3eFidQsGVFl1glijA/TRdH8tG1SbC +lGO+FbtsPu7ZrMGGwm5u2mEocYrKXh7pm/Ht2jWFRA0pHKYXEKmxf87VKKroXrgh +cLXecky6bveEgCNC6LeBG00bjex4Y0jbINi3211NAoIBAC9ASRIi3LTgLVk8sEMg +fZGrvnricUysRBvMd0lsp2mbEu99R8SD11eBL4qmWYk0UQc+ragZgwlRFDF26u+n +fCQ32Mri2sdF41EO1bjQRW30wj4CMkS9z+i2qZYG8KLFFE0xs/VHe7QwAUTsLUQJ +dUGcrN28rt03/iYTo8Mdarsg9TPjotBISQ9GtYR5T61WDQLNLW8OfqcEwX0MDEsQ +O54k9Y4C6B/ml09qrytf0kFlE3w5CAOo+INLygU0U51EWsjijhoh5ouaw5peDva4 +C/EKsafPLhzWVH0plh/JSdRBxHzEEooYyTOz0ImfGkJjNoGvUNrpZ0XxkCAMSg4c +OGECggEBAPlRKngkZoGz10uvloDe8djqpLlR4faNsbuzxeFmGteLl+oUQvCwwGPZ +/SXpXBAVk9uGzrOpO4irwnBc+Pnx2StwgCC+NpItf37+XmraAarLWj8LQQctwBF3 +aq1pbPgp4pfSeVT+WKsKqFCzKHDsehOubLg9F5GPrBTfiskwM2U3CNLQy+29PQKM +FpDHCF37JF2135hufza+oLL6VoXeSi6Q+oh8I3LCS+vJ05u5+HoAv9YC51RGBhYy +tgYUimqzMRPWfTNkfH+LTYLmzoPvaStkbvIasiDZB/s9ejdDl6MEFpnte1VSK3jC +4VvGnkGGbUe3PWfxA+yBJqdyprlXbaY= +-----END PRIVATE KEY----- diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx new file mode 100644 index 000000000..b3032a3e2 Binary files /dev/null and b/backend/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx differ diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminHost.cs b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminHost.cs new file mode 100644 index 000000000..164849ba8 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminHost.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Logging; +using Squidex.Config; +using Squidex.Config.Startup; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +namespace Squidex.Areas.IdentityServer.Config +{ + public sealed class CreateAdminHost : SafeHostedService + { + private readonly IServiceProvider serviceProvider; + private readonly MyIdentityOptions identityOptions; + + public CreateAdminHost(ISemanticLog log, IServiceProvider serviceProvider, IOptions identityOptions) + : base(log) + { + this.serviceProvider = serviceProvider; + + this.identityOptions = identityOptions.Value; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + IdentityModelEventSource.ShowPII = identityOptions.ShowPII; + + if (identityOptions.IsAdminConfigured()) + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + var userFactory = scope.ServiceProvider.GetRequiredService(); + + var adminEmail = identityOptions.AdminEmail; + var adminPass = identityOptions.AdminPassword; + + if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) + { + try + { + var values = new UserValues + { + Email = adminEmail, + Password = adminPass, + Permissions = new PermissionSet(Permissions.Admin), + DisplayName = adminEmail + }; + + await userManager.CreateAsync(userFactory, values); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "createAdmin") + .WriteProperty("status", "failed")); + } + } + } + } + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs new file mode 100644 index 000000000..aca245734 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Logging; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +namespace Squidex.Areas.IdentityServer.Config +{ + public static class IdentityServerExtensions + { + public static IApplicationBuilder UseSquidexIdentityServer(this IApplicationBuilder app) + { + app.UseIdentityServer(); + + return app; + } + + public static IServiceProvider UseSquidexAdmin(this IServiceProvider services) + { + var options = services.GetRequiredService>().Value; + + IdentityModelEventSource.ShowPII = options.ShowPII; + + var userManager = services.GetRequiredService>(); + var userFactory = services.GetRequiredService(); + + var log = services.GetRequiredService(); + + if (options.IsAdminConfigured()) + { + var adminEmail = options.AdminEmail; + var adminPass = options.AdminPassword; + + Task.Run(async () => + { + if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) + { + try + { + var values = new UserValues + { + Email = adminEmail, + Password = adminPass, + Permissions = new PermissionSet(Permissions.Admin), + DisplayName = adminEmail + }; + + await userManager.CreateAsync(userFactory, values); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "createAdmin") + .WriteProperty("status", "failed")); + } + } + }).Wait(); + } + + return services; + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs new file mode 100644 index 000000000..8127fbe1a --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -0,0 +1,120 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using IdentityModel; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Users; +using Squidex.Shared.Identity; +using Squidex.Web; +using Squidex.Web.Pipeline; + +namespace Squidex.Areas.IdentityServer.Config +{ + public static class IdentityServerServices + { + public static void AddSquidexIdentityServer(this IServiceCollection services) + { + X509Certificate2 certificate; + + var assembly = typeof(IdentityServerServices).Assembly; + + using (var certificateStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx")) + { + var certData = new byte[certificateStream!.Length]; + + certificateStream.Read(certData, 0, certData.Length); + certificate = new X509Certificate2(certData, "password", + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); + } + + services.AddSingleton>(s => + { + return new ConfigureOptions(options => + { + options.XmlRepository = s.GetRequiredService(); + }); + }); + + services.AddDataProtection().SetApplicationName("Squidex"); + + services.AddSingleton(GetApiResources()); + services.AddSingleton(GetIdentityResources()); + + services.AddIdentity() + .AddDefaultTokenProviders(); + services.AddSingleton, + PwnedPasswordValidator>(); + services.AddScoped, + UserClaimsPrincipalFactoryWithEmail>(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddIdentityServer(options => + { + options.UserInteraction.ErrorUrl = "/error/"; + }) + .AddAspNetIdentity() + .AddInMemoryApiResources(GetApiResources()) + .AddInMemoryIdentityResources(GetIdentityResources()) + .AddSigningCredential(certificate); + } + + private static IEnumerable GetApiResources() + { + yield return new ApiResource(Constants.ApiScope) + { + UserClaims = new List + { + JwtClaimTypes.Email, + JwtClaimTypes.Role, + SquidexClaimTypes.Permissions + } + }; + } + + private static IEnumerable GetIdentityResources() + { + yield return new IdentityResources.OpenId(); + yield return new IdentityResources.Profile(); + yield return new IdentityResources.Email(); + yield return new IdentityResource(Constants.RoleScope, + new[] + { + JwtClaimTypes.Role + }); + yield return new IdentityResource(Constants.PermissionsScope, + new[] + { + SquidexClaimTypes.Permissions + }); + yield return new IdentityResource(Constants.ProfileScope, + new[] + { + SquidexClaimTypes.DisplayName, + SquidexClaimTypes.PictureUrl + }); + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs new file mode 100644 index 000000000..86642263c --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -0,0 +1,239 @@ +// ========================================================================== +// 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.Security.Claims; +using System.Threading.Tasks; +using IdentityServer4; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Config; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.IdentityServer.Config +{ + public class LazyClientStore : IClientStore + { + private readonly IServiceProvider serviceProvider; + private readonly IAppProvider appProvider; + private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public LazyClientStore( + IServiceProvider serviceProvider, + IOptions urlsOptions, + IOptions identityOptions, + IAppProvider appProvider) + { + Guard.NotNull(appProvider); + Guard.NotNull(identityOptions); + Guard.NotNull(serviceProvider); + Guard.NotNull(urlsOptions); + + this.serviceProvider = serviceProvider; + + this.appProvider = appProvider; + + CreateStaticClients(urlsOptions, identityOptions); + } + + public async Task FindClientByIdAsync(string clientId) + { + var client = staticClients.GetOrDefault(clientId); + + if (client != null) + { + return client; + } + + var (appName, appClientId) = clientId.GetClientParts(); + + if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId)) + { + var app = await appProvider.GetAppAsync(appName); + + var appClient = app?.Clients.GetOrDefault(appClientId); + + if (appClient != null) + { + return CreateClientFromApp(clientId, appClient); + } + } + + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var user = await userManager.FindByIdWithClaimsAsync(clientId); + + if (!string.IsNullOrWhiteSpace(user?.ClientSecret())) + { + return CreateClientFromUser(user); + } + } + + return null; + } + + private static Client CreateClientFromUser(UserWithClaims user) + { + return new Client + { + ClientId = user.Id, + ClientName = $"{user.Email} Client", + ClientClaimsPrefix = null, + ClientSecrets = new List + { + new Secret(user.ClientSecret().Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + }, + Claims = new List + { + new Claim(OpenIdClaims.Subject, user.Id) + } + }; + } + + private static Client CreateClientFromApp(string id, AppClient appClient) + { + return new Client + { + ClientId = id, + ClientName = id, + ClientSecrets = new List + { + new Secret(appClient.Secret.Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + } + }; + } + + private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions) + { + foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value)) + { + staticClients[client.ClientId] = client; + } + } + + private static IEnumerable CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions) + { + var frontendId = Constants.FrontendClient; + + yield return new Client + { + ClientId = frontendId, + ClientName = frontendId, + RedirectUris = new List + { + urlsOptions.BuildUrl("login;"), + urlsOptions.BuildUrl("client-callback-silent", false), + urlsOptions.BuildUrl("client-callback-popup", false) + }, + PostLogoutRedirectUris = new List + { + urlsOptions.BuildUrl("logout", false) + }, + AllowAccessTokensViaBrowser = true, + AllowedGrantTypes = GrantTypes.Implicit, + AllowedScopes = new List + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + Constants.ApiScope, + Constants.PermissionsScope, + Constants.ProfileScope, + Constants.RoleScope + }, + RequireConsent = false + }; + + var internalClient = Constants.InternalClientId; + + yield return new Client + { + ClientId = internalClient, + ClientName = internalClient, + ClientSecrets = new List + { + new Secret(Constants.InternalClientSecret) + }, + RedirectUris = new List + { + urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false), + urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, + AllowedScopes = new List + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + Constants.ApiScope, + Constants.PermissionsScope, + Constants.ProfileScope, + Constants.RoleScope + }, + RequireConsent = false + }; + + if (identityOptions.IsAdminClientConfigured()) + { + var id = identityOptions.AdminClientId; + + yield return new Client + { + ClientId = id, + ClientName = id, + ClientSecrets = new List + { + new Secret(identityOptions.AdminClientSecret.Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + }, + Claims = new List + { + new Claim(SquidexClaimTypes.Permissions, Permissions.All) + } + }; + } + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs new file mode 100644 index 000000000..983e56954 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -0,0 +1,438 @@ +// ========================================================================== +// 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 System.Runtime.CompilerServices; +using System.Security; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using Squidex.Web; + +namespace Squidex.Areas.IdentityServer.Controllers.Account +{ + public sealed class AccountController : IdentityServerController + { + private readonly SignInManager signInManager; + private readonly UserManager userManager; + private readonly IUserFactory userFactory; + private readonly IUserEvents userEvents; + private readonly MyIdentityOptions identityOptions; + private readonly ISemanticLog log; + private readonly IIdentityServerInteractionService interactions; + + public AccountController( + SignInManager signInManager, + UserManager userManager, + IUserFactory userFactory, + IUserEvents userEvents, + IOptions identityOptions, + ISemanticLog log, + IIdentityServerInteractionService interactions) + { + this.log = log; + this.userEvents = userEvents; + this.userManager = userManager; + this.userFactory = userFactory; + this.interactions = interactions; + this.identityOptions = identityOptions.Value; + this.signInManager = signInManager; + } + + [HttpGet] + [Route("account/error/")] + public IActionResult LoginError() + { + throw new InvalidOperationException(); + } + + [HttpGet] + [Route("account/forbidden/")] + public IActionResult Forbidden() + { + throw new SecurityException("User is not allowed to login."); + } + + [HttpGet] + [Route("account/lockedout/")] + public IActionResult LockedOut() + { + return View(); + } + + [HttpGet] + [Route("account/accessdenied/")] + public IActionResult AccessDenied() + { + return View(); + } + + [HttpGet] + [Route("account/logout-completed/")] + public IActionResult LogoutCompleted() + { + return View(); + } + + [HttpGet] + [Route("account/consent/")] + public IActionResult Consent(string? returnUrl = null) + { + return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); + } + + [HttpPost] + [Route("account/consent/")] + public async Task Consent(ConsentModel model, string? returnUrl = null) + { + if (!model.ConsentToCookies) + { + ModelState.AddModelError(nameof(model.ConsentToCookies), "You have to give consent."); + } + + if (!model.ConsentToPersonalInformation) + { + ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), "You have to give consent."); + } + + if (!ModelState.IsValid) + { + var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; + + return View(vm); + } + + var user = await userManager.GetUserWithClaimsAsync(User); + + if (user == null) + { + throw new DomainException("Cannot find user."); + } + + var update = new UserValues + { + Consent = true, + ConsentForEmails = model.ConsentToAutomatedEmails + }; + + await userManager.UpdateAsync(user.Id, update); + + userEvents.OnConsentGiven(user); + + return RedirectToReturnUrl(returnUrl); + } + + [HttpGet] + [Route("account/logout/")] + public async Task Logout(string logoutId) + { + var context = await interactions.GetLogoutContextAsync(logoutId); + + await signInManager.SignOutAsync(); + + return RedirectToLogoutUrl(context); + } + + [HttpGet] + [Route("account/logout-redirect/")] + public async Task LogoutRedirect() + { + await signInManager.SignOutAsync(); + + return RedirectToAction(nameof(LogoutCompleted)); + } + + [HttpGet] + [Route("account/signup/")] + public Task Signup(string? returnUrl = null) + { + return LoginViewAsync(returnUrl, false, false); + } + + [HttpGet] + [Route("account/login/")] + [ClearCookies] + public Task Login(string? returnUrl = null) + { + return LoginViewAsync(returnUrl, true, false); + } + + [HttpPost] + [Route("account/login/")] + public async Task Login(LoginModel model, string? returnUrl = null) + { + if (!ModelState.IsValid) + { + return await LoginViewAsync(returnUrl, true, true); + } + + var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, true, true); + + if (!result.Succeeded) + { + return await LoginViewAsync(returnUrl, true, true); + } + else + { + return RedirectToReturnUrl(returnUrl); + } + } + + private async Task LoginViewAsync(string? returnUrl, bool isLogin, bool isFailed) + { + var allowPasswordAuth = identityOptions.AllowPasswordAuth; + + var externalProviders = await signInManager.GetExternalProvidersAsync(); + + if (externalProviders.Count == 1 && !allowPasswordAuth) + { + var provider = externalProviders[0].AuthenticationScheme; + + var properties = + signInManager.ConfigureExternalAuthenticationProperties(provider, + Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); + + return Challenge(properties, provider); + } + + var vm = new LoginVM + { + ExternalProviders = externalProviders, + IsLogin = isLogin, + IsFailed = isFailed, + HasPasswordAuth = allowPasswordAuth, + HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(), + ReturnUrl = returnUrl + }; + + return View(nameof(Login), vm); + } + + [HttpPost] + [Route("account/external/")] + public IActionResult External(string provider, string? returnUrl = null) + { + var properties = + signInManager.ConfigureExternalAuthenticationProperties(provider, + Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); + + return Challenge(properties, provider); + } + + [HttpGet] + [Route("account/external-callback/")] + public async Task ExternalCallback(string? returnUrl = null) + { + var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(); + + if (externalLogin == null) + { + return RedirectToAction(nameof(Login)); + } + + var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + + if (!result.Succeeded && result.IsLockedOut) + { + return View(nameof(LockedOut)); + } + + var isLoggedIn = result.Succeeded; + + UserWithClaims? user; + + if (isLoggedIn) + { + user = await userManager.FindByLoginWithClaimsAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); + } + else + { + var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; + + user = await userManager.FindByEmailWithClaimsAsyncAsync(email); + + if (user != null) + { + isLoggedIn = + await AddLoginAsync(user, externalLogin) && + await AddClaimsAsync(user, externalLogin, email) && + await LoginAsync(externalLogin); + } + else + { + user = new UserWithClaims(userFactory.Create(email), new List()); + + var isFirst = userManager.Users.LongCount() == 0; + + isLoggedIn = + await AddUserAsync(user) && + await AddLoginAsync(user, externalLogin) && + await AddClaimsAsync(user, externalLogin, email, isFirst) && + await LockAsync(user, isFirst) && + await LoginAsync(externalLogin); + + userEvents.OnUserRegistered(user); + + if (await userManager.IsLockedOutAsync(user.Identity)) + { + return View(nameof(LockedOut)); + } + } + } + + if (!isLoggedIn) + { + return RedirectToAction(nameof(Login)); + } + else if (user != null && !user.HasConsent() && !identityOptions.NoConsent) + { + return RedirectToAction(nameof(Consent), new { returnUrl }); + } + else + { + return RedirectToReturnUrl(returnUrl); + } + } + + private Task AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin) + { + return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin)); + } + + private Task AddUserAsync(UserWithClaims user) + { + return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity)); + } + + private async Task LoginAsync(UserLoginInfo externalLogin) + { + var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + + return result.Succeeded; + } + + private Task LockAsync(UserWithClaims user, bool isFirst) + { + if (isFirst || !identityOptions.LockAutomatically) + { + return TaskHelper.True; + } + + return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); + } + + private Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) + { + var newClaims = new List(); + + void AddClaim(Claim claim) + { + newClaims.Add(claim); + + user.Claims.Add(claim); + } + + foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims()) + { + AddClaim(squidexClaim); + } + + if (!user.HasPictureUrl()) + { + AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email))); + } + + if (!user.HasDisplayName()) + { + AddClaim(new Claim(SquidexClaimTypes.DisplayName, email)); + } + + if (isFirst) + { + AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)); + } + + return MakeIdentityOperation(() => userManager.SyncClaimsAsync(user.Identity, newClaims)); + } + + private IActionResult RedirectToLogoutUrl(LogoutRequest context) + { + if (!string.IsNullOrWhiteSpace(context.PostLogoutRedirectUri)) + { + return Redirect(context.PostLogoutRedirectUri); + } + else + { + return Redirect("~/../"); + } + } + + private IActionResult RedirectToReturnUrl(string? returnUrl) + { + if (!string.IsNullOrWhiteSpace(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return Redirect("~/../"); + } + } + + private async Task MakeIdentityOperation(Func> action, [CallerMemberName] string? operationName = null) + { + try + { + var result = await action(); + + if (!result.Succeeded) + { + var errorMessageBuilder = new StringBuilder(); + + foreach (var error in result.Errors) + { + errorMessageBuilder.Append(error.Code); + errorMessageBuilder.Append(": "); + errorMessageBuilder.AppendLine(error.Description); + } + + var errorMessage = errorMessageBuilder.ToString(); + + log.LogError((operationName, errorMessage), (ctx, w) => w + .WriteProperty("action", ctx.operationName) + .WriteProperty("status", "Failed") + .WriteProperty("message", ctx.errorMessage)); + } + + return result.Succeeded; + } + catch (Exception ex) + { + log.LogError(ex, operationName, (logOperationName, w) => w + .WriteProperty("action", logOperationName) + .WriteProperty("status", "Failed")); + + return false; + } + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs new file mode 100644 index 000000000..92aba14ae --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Areas.IdentityServer.Controllers.Account +{ + public sealed class ConsentVM + { + public string? ReturnUrl { get; set; } + + public string? PrivacyUrl { get; set; } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs new file mode 100644 index 000000000..35d8cc8f5 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Areas.IdentityServer.Controllers.Account +{ + public class LoginVM + { + public string? ReturnUrl { get; set; } + + public bool IsLogin { get; set; } + + public bool IsFailed { get; set; } + + public bool HasPasswordAuth { get; set; } + + public bool HasPasswordAndExternal { get; set; } + + public IReadOnlyList ExternalProviders { get; set; } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs new file mode 100644 index 000000000..528d504d2 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Squidex.Infrastructure; + +namespace Squidex.Areas.IdentityServer.Controllers.Error +{ + public sealed class ErrorController : IdentityServerController + { + private readonly IIdentityServerInteractionService interaction; + private readonly SignInManager signInManager; + + public ErrorController(IIdentityServerInteractionService interaction, SignInManager signInManager) + { + this.interaction = interaction; + this.signInManager = signInManager; + } + + [Route("error/")] + public async Task Error(string? errorId = null) + { + await signInManager.SignOutAsync(); + + var vm = new ErrorViewModel(); + + if (!string.IsNullOrWhiteSpace(errorId)) + { + var message = await interaction.GetErrorContextAsync(errorId); + + if (message != null) + { + vm.Error = message; + } + } + + if (vm.Error == null) + { + var error = HttpContext.Features.Get()?.Error; + + if (error is DomainException exception) + { + vm.Error = new ErrorMessage { ErrorDescription = exception.Message }; + } + else if (error?.InnerException is DomainException exception2) + { + vm.Error = new ErrorMessage { ErrorDescription = exception2.Message }; + } + } + + return View("Error", vm); + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs new file mode 100644 index 000000000..c7daea59b --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// 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 System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Identity; + +namespace Squidex.Areas.IdentityServer.Controllers +{ + public static class Extensions + { + public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string? expectedXsrf = null) + { + var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); + + var email = externalLogin.Principal.FindFirst(ClaimTypes.Email)?.Value; + + if (string.IsNullOrWhiteSpace(email)) + { + throw new InvalidOperationException("External provider does not provide email claim."); + } + + externalLogin.ProviderDisplayName = email; + + return externalLogin; + } + + public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) + { + var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); + + var externalProviders = + externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) + .Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); + + return externalProviders; + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs new file mode 100644 index 000000000..41e22fa3a --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -0,0 +1,235 @@ +// ========================================================================== +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; + +namespace Squidex.Areas.IdentityServer.Controllers.Profile +{ + [Authorize] + public sealed class ProfileController : IdentityServerController + { + private readonly SignInManager signInManager; + private readonly UserManager userManager; + private readonly IUserPictureStore userPictureStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly MyIdentityOptions identityOptions; + + public ProfileController( + SignInManager signInManager, + UserManager userManager, + IUserPictureStore userPictureStore, + IAssetThumbnailGenerator assetThumbnailGenerator, + IOptions identityOptions) + { + this.signInManager = signInManager; + this.identityOptions = identityOptions.Value; + this.userManager = userManager; + this.userPictureStore = userPictureStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + [HttpGet] + [Route("/account/profile/")] + public async Task Profile(string? successMessage = null) + { + var user = await userManager.GetUserWithClaimsAsync(User); + + return View(await GetProfileVM(user, successMessage: successMessage)); + } + + [HttpPost] + [Route("/account/profile/login-add/")] + public async Task AddLogin(string provider) + { + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + var properties = + signInManager.ConfigureExternalAuthenticationProperties(provider, + Url.Action(nameof(AddLoginCallback)), userManager.GetUserId(User)); + + return Challenge(properties, provider); + } + + [HttpGet] + [Route("/account/profile/login-add-callback/")] + public Task AddLoginCallback() + { + return MakeChangeAsync(user => AddLoginAsync(user), + "Login added successfully."); + } + + [HttpPost] + [Route("/account/profile/update/")] + public Task UpdateProfile(ChangeProfileModel model) + { + return MakeChangeAsync(user => userManager.UpdateSafeAsync(user.Identity, model.ToValues()), + "Account updated successfully."); + } + + [HttpPost] + [Route("/account/profile/login-remove/")] + public Task RemoveLogin(RemoveLoginModel model) + { + return MakeChangeAsync(user => userManager.RemoveLoginAsync(user.Identity, model.LoginProvider, model.ProviderKey), + "Login provider removed successfully."); + } + + [HttpPost] + [Route("/account/profile/password-set/")] + public Task SetPassword(SetPasswordModel model) + { + return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password), + "Password set successfully."); + } + + [HttpPost] + [Route("/account/profile/password-change/")] + public Task ChangePassword(ChangePasswordModel model) + { + return MakeChangeAsync(user => userManager.ChangePasswordAsync(user.Identity, model.OldPassword, model.Password), + "Password changed successfully."); + } + + [HttpPost] + [Route("/account/profile/generate-client-secret/")] + public Task GenerateClientSecret() + { + return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity), + "Client secret generated successfully."); + } + + [HttpPost] + [Route("/account/profile/upload-picture/")] + public Task UploadPicture(List file) + { + return MakeChangeAsync(user => UpdatePictureAsync(file, user), + "Picture uploaded successfully."); + } + + private async Task AddLoginAsync(UserWithClaims user) + { + var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User)); + + return await userManager.AddLoginAsync(user.Identity, externalLogin); + } + + private async Task UpdatePictureAsync(List file, UserWithClaims user) + { + if (file.Count != 1) + { + return IdentityResult.Failed(new IdentityError { Description = "Please upload a single file." }); + } + + using (var thumbnailStream = new MemoryStream()) + { + try + { + await assetThumbnailGenerator.CreateThumbnailAsync(file[0].OpenReadStream(), thumbnailStream, 128, 128, "Crop"); + + thumbnailStream.Position = 0; + } + catch + { + return IdentityResult.Failed(new IdentityError { Description = "Picture is not a valid image." }); + } + + await userPictureStore.UploadAsync(user.Id, thumbnailStream); + } + + return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }); + } + + private async Task MakeChangeAsync(Func> action, string successMessage, ChangeProfileModel? model = null) + { + var user = await userManager.GetUserWithClaimsAsync(User); + + if (user == null) + { + throw new DomainException("Cannot find user."); + } + + if (!ModelState.IsValid) + { + return View(nameof(Profile), await GetProfileVM(user, model)); + } + + string errorMessage; + try + { + var result = await action(user); + + if (result.Succeeded) + { + await signInManager.SignInAsync(user.Identity, true); + + return RedirectToAction(nameof(Profile), new { successMessage }); + } + + errorMessage = string.Join(". ", result.Errors.Select(x => x.Description)); + } + catch + { + errorMessage = "An unexpected exception occurred."; + } + + return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); + } + + private async Task GetProfileVM(UserWithClaims? user, ChangeProfileModel? model = null, string? errorMessage = null, string? successMessage = null) + { + if (user == null) + { + throw new DomainException("Cannot find user."); + } + + var taskForProviders = signInManager.GetExternalProvidersAsync(); + var taskForPassword = userManager.HasPasswordAsync(user.Identity); + var taskForLogins = userManager.GetLoginsAsync(user.Identity); + + await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins); + + var result = new ProfileVM + { + Id = user.Id, + ClientSecret = user.ClientSecret()!, + Email = user.Email, + ErrorMessage = errorMessage, + ExternalLogins = taskForLogins.Result, + ExternalProviders = taskForProviders.Result, + DisplayName = user.DisplayName()!, + IsHidden = user.IsHidden(), + HasPassword = taskForPassword.Result, + HasPasswordAuth = identityOptions.AllowPasswordAuth, + SuccessMessage = successMessage + }; + + if (model != null) + { + SimpleMapper.Map(model, result); + } + + return result; + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs new file mode 100644 index 000000000..d2f377e8d --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; + +namespace Squidex.Areas.IdentityServer.Controllers.Profile +{ + public sealed class ProfileVM + { + public string Id { get; set; } + + public string Email { get; set; } + + public string DisplayName { get; set; } + + public string? ClientSecret { get; set; } + + public string? ErrorMessage { get; set; } + + public string? SuccessMessage { get; set; } + + public bool IsHidden { get; set; } + + public bool HasPassword { get; set; } + + public bool HasPasswordAuth { get; set; } + + public IList ExternalLogins { get; set; } + + public IList ExternalProviders { get; set; } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs similarity index 100% rename from src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs diff --git a/backend/src/Squidex/Areas/IdentityServer/Startup.cs b/backend/src/Squidex/Areas/IdentityServer/Startup.cs new file mode 100644 index 000000000..75a89085e --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Startup.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Web; + +namespace Squidex.Areas.IdentityServer +{ + public static class Startup + { + public static void ConfigureIdentityServer(this IApplicationBuilder app) + { + var environment = app.ApplicationServices.GetRequiredService(); + + app.Map(Constants.IdentityServerPrefix, identityApp => + { + if (!environment.IsDevelopment()) + { + identityApp.UseDeveloperExceptionPage(); + } + else + { + identityApp.UseExceptionHandler("/error"); + } + + identityApp.UseRouting(); + + identityApp.UseAuthentication(); + identityApp.UseAuthorization(); + + identityApp.UseSquidexIdentityServer(); + + identityApp.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + }); + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs b/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs new file mode 100644 index 000000000..eb16259f5 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Squidex.Areas.IdentityServer.Views +{ + public static class Extensions + { + public static string? RootContentUrl(this IUrlHelper urlHelper, string contentPath) + { + if (string.IsNullOrEmpty(contentPath)) + { + return null; + } + + if (contentPath[0] == '~') + { + var segment = new PathString(contentPath.Substring(1)); + + var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase; + + if (applicationPath.HasValue) + { + var indexOfLastPart = applicationPath.Value.LastIndexOf('/'); + + if (indexOfLastPart >= 0) + { + applicationPath = applicationPath.Value.Substring(0, indexOfLastPart); + } + } + + return applicationPath.Add(segment).Value; + } + + return contentPath; + } + } +} diff --git a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml diff --git a/src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml similarity index 100% rename from src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml rename to backend/src/Squidex/Areas/IdentityServer/Views/_ViewStart.cshtml diff --git a/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs b/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs similarity index 100% rename from src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs rename to backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs diff --git a/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs b/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs new file mode 100644 index 000000000..9285b5ae8 --- /dev/null +++ b/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Orleans; +using Squidex.Areas.OrleansDashboard.Middlewares; +using Squidex.Web; + +namespace Squidex.Areas.OrleansDashboard +{ + public static class Startup + { + public static void ConfigureOrleansDashboard(this IApplicationBuilder app) + { + app.Map(Constants.OrleansPrefix, orleansApp => + { + orleansApp.UseAuthentication(); + orleansApp.UseAuthorization(); + + orleansApp.UseMiddleware(); + orleansApp.UseOrleansDashboard(); + }); + } + } +} diff --git a/src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs b/backend/src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs similarity index 100% rename from src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs rename to backend/src/Squidex/Areas/Portal/Middlewares/PortalDashboardAuthenticationMiddleware.cs diff --git a/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs new file mode 100644 index 000000000..7e6ac990b --- /dev/null +++ b/backend/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Apps.Services; + +namespace Squidex.Areas.Portal.Middlewares +{ + public sealed class PortalRedirectMiddleware + { + private readonly IAppPlanBillingManager appPlansBillingManager; + + public PortalRedirectMiddleware(RequestDelegate next, IAppPlanBillingManager appPlansBillingManager) + { + this.appPlansBillingManager = appPlansBillingManager; + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Path == "/") + { + var userIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier); + + if (userIdClaim != null) + { + var portalLink = await appPlansBillingManager.GetPortalLinkAsync(userIdClaim.Value); + + context.Response.Redirect(portalLink); + } + } + } + } +} diff --git a/backend/src/Squidex/Areas/Portal/Startup.cs b/backend/src/Squidex/Areas/Portal/Startup.cs new file mode 100644 index 000000000..7c3f20512 --- /dev/null +++ b/backend/src/Squidex/Areas/Portal/Startup.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Squidex.Areas.Portal.Middlewares; +using Squidex.Web; + +namespace Squidex.Areas.Portal +{ + public static class Startup + { + public static void ConfigurePortal(this IApplicationBuilder app) + { + app.Map(Constants.PortalPrefix, portalApp => + { + portalApp.UseAuthentication(); + portalApp.UseAuthorization(); + + portalApp.UseMiddleware(); + portalApp.UseMiddleware(); + }); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs new file mode 100644 index 000000000..0e27dfb7e --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Users; + +namespace Squidex.Config.Authentication +{ + public static class AuthenticationServices + { + public static void AddSquidexAuthentication(this IServiceCollection services, IConfiguration config) + { + var identityOptions = config.GetSection("identity").Get(); + + services.AddSingletonAs() + .As(); + + services.AddAuthentication() + .AddSquidexCookies() + .AddSquidexExternalGithubAuthentication(identityOptions) + .AddSquidexExternalGoogleAuthentication(identityOptions) + .AddSquidexExternalMicrosoftAuthentication(identityOptions) + .AddSquidexExternalOdic(identityOptions) + .AddSquidexIdentityServerAuthentication(identityOptions, config); + } + + public static AuthenticationBuilder AddSquidexCookies(this AuthenticationBuilder builder) + { + builder.Services.ConfigureApplicationCookie(options => + { + options.Cookie.Name = ".sq.auth"; + }); + + return builder.AddCookie(); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs new file mode 100644 index 000000000..49beaa3bd --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class GithubAuthenticationServices + { + public static AuthenticationBuilder AddSquidexExternalGithubAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsGithubAuthConfigured()) + { + authBuilder.AddGitHub(options => + { + options.ClientId = identityOptions.GithubClient; + options.ClientSecret = identityOptions.GithubSecret; + options.Events = new GithubHandler(); + }); + } + + return authBuilder; + } + } +} diff --git a/src/Squidex/Config/Authentication/GithubHandler.cs b/backend/src/Squidex/Config/Authentication/GithubHandler.cs similarity index 100% rename from src/Squidex/Config/Authentication/GithubHandler.cs rename to backend/src/Squidex/Config/Authentication/GithubHandler.cs diff --git a/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs new file mode 100644 index 000000000..d175d92eb --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class GoogleAuthenticationServices + { + public static AuthenticationBuilder AddSquidexExternalGoogleAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsGoogleAuthConfigured()) + { + authBuilder.AddGoogle(options => + { + options.ClientId = identityOptions.GoogleClient; + options.ClientSecret = identityOptions.GoogleSecret; + options.Events = new GoogleHandler(); + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/GoogleHandler.cs b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs new file mode 100644 index 000000000..a8da64c84 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared.Identity; + +namespace Squidex.Config.Authentication +{ + public sealed class GoogleHandler : OAuthEvents + { + public override Task RedirectToAuthorizationEndpoint(RedirectContext context) + { + context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); + + return TaskHelper.Done; + } + + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + var nameClaim = context.Identity.FindFirst(ClaimTypes.Name)?.Value; + + if (!string.IsNullOrWhiteSpace(nameClaim)) + { + context.Identity.SetDisplayName(nameClaim); + } + + string? pictureUrl = null; + + if (context.User.TryGetProperty("picture", out var picture) && picture.ValueKind == JsonValueKind.String) + { + pictureUrl = picture.GetString(); + } + + if (string.IsNullOrWhiteSpace(pictureUrl)) + { + if (context.User.TryGetProperty("image", out var image) && image.TryGetProperty("url", out var url) && url.ValueKind == JsonValueKind.String) + { + pictureUrl = url.GetString(); + } + + if (pictureUrl != null && pictureUrl.EndsWith("?sz=50", System.StringComparison.Ordinal)) + { + pictureUrl = pictureUrl[0..^6]; + } + } + + if (!string.IsNullOrWhiteSpace(pictureUrl)) + { + context.Identity.SetPictureUrl(pictureUrl); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs new file mode 100644 index 000000000..bfa5720f4 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Web; + +namespace Squidex.Config.Authentication +{ + public static class IdentityServerServices + { + public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) + { + var apiScope = Constants.ApiScope; + + var urlsOptions = config.GetSection("urls").Get(); + + if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl)) + { + string apiAuthorityUrl; + + if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl)) + { + apiAuthorityUrl = identityOptions.AuthorityUrl.BuildFullUrl(Constants.IdentityServerPrefix); + } + else + { + apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix); + } + + authBuilder.AddIdentityServerAuthentication(options => + { + options.Authority = apiAuthorityUrl; + options.ApiName = apiScope; + options.ApiSecret = null; + options.RequireHttpsMetadata = identityOptions.RequiresHttps; + }); + + authBuilder.AddOpenIdConnect(options => + { + options.Authority = apiAuthorityUrl; + options.ClientId = Constants.InternalClientId; + options.ClientSecret = Constants.InternalClientSecret; + options.CallbackPath = "/signin-internal"; + options.RequireHttpsMetadata = identityOptions.RequiresHttps; + options.SaveTokens = true; + options.Scope.Add(Constants.PermissionsScope); + options.Scope.Add(Constants.ProfileScope); + options.Scope.Add(Constants.RoleScope); + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/IdentityServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServices.cs new file mode 100644 index 000000000..43ad4a929 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/IdentityServices.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Users; +using Squidex.Shared.Users; + +namespace Squidex.Config.Authentication +{ + public static class IdentityServices + { + public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("identity")); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs new file mode 100644 index 000000000..0f03f68a0 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class MicrosoftAuthenticationServices + { + public static AuthenticationBuilder AddSquidexExternalMicrosoftAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsMicrosoftAuthConfigured()) + { + authBuilder.AddMicrosoftAccount(options => + { + options.ClientId = identityOptions.MicrosoftClient; + options.ClientSecret = identityOptions.MicrosoftSecret; + options.Events = new MicrosoftHandler(); + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs new file mode 100644 index 000000000..718924871 --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Shared.Identity; + +namespace Squidex.Config.Authentication +{ + public sealed class MicrosoftHandler : OAuthEvents + { + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + string? displayName = null; + + if (context.User.TryGetProperty("displayName", out var element1) && element1.ValueKind == JsonValueKind.String) + { + displayName = element1.GetString(); + } + + if (!string.IsNullOrEmpty(displayName)) + { + context.Identity.SetDisplayName(displayName); + } + + string? id = null; + + if (context.User.TryGetProperty("id", out var element2) && element2.ValueKind == JsonValueKind.String) + { + id = element2.GetString(); + } + + if (!string.IsNullOrEmpty(id)) + { + var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; + + context.Identity.SetPictureUrl(pictureUrl); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/src/Squidex/Config/Authentication/OidcHandler.cs b/backend/src/Squidex/Config/Authentication/OidcHandler.cs similarity index 100% rename from src/Squidex/Config/Authentication/OidcHandler.cs rename to backend/src/Squidex/Config/Authentication/OidcHandler.cs diff --git a/backend/src/Squidex/Config/Authentication/OidcServices.cs b/backend/src/Squidex/Config/Authentication/OidcServices.cs new file mode 100644 index 000000000..00d82cbcc --- /dev/null +++ b/backend/src/Squidex/Config/Authentication/OidcServices.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Config.Authentication +{ + public static class OidcServices + { + public static AuthenticationBuilder AddSquidexExternalOdic(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + { + if (identityOptions.IsOidcConfigured()) + { + var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; + + authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => + { + options.Authority = identityOptions.OidcAuthority; + options.ClientId = identityOptions.OidcClient; + options.ClientSecret = identityOptions.OidcSecret; + options.RequireHttpsMetadata = false; + options.Events = new OidcHandler(identityOptions); + + if (identityOptions.OidcScopes != null) + { + foreach (var scope in identityOptions.OidcScopes) + { + options.Scope.Add(scope); + } + } + }); + } + + return authBuilder; + } + } +} diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs new file mode 100644 index 000000000..e7e7a6001 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Areas.Api.Controllers.UI; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.History; + +namespace Squidex.Config.Domain +{ + public static class AppsServices + { + public static void AddSquidexApps(this IServiceCollection services) + { + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingleton(c => + { + var uiOptions = c.GetRequiredService>().Value; + + var result = new InitialPatterns(); + + if (uiOptions.RegexSuggestions != null) + { + foreach (var (key, value) in uiOptions.RegexSuggestions) + { + if (!string.IsNullOrWhiteSpace(key) && + !string.IsNullOrWhiteSpace(value)) + { + result[Guid.NewGuid()] = new AppPattern(key, value); + } + } + } + + return result; + }); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs new file mode 100644 index 000000000..2e9251998 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using FluentFTP; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Queries; +using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Assets.ImageSharp; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Domain +{ + public static class AssetServices + { + public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("assets")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As>(); + + services.AddSingletonAs() + .As>(); + } + + public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("assetStore:type", new Alternatives + { + ["Default"] = () => + { + services.AddSingletonAs() + .AsOptional(); + }, + ["Folder"] = () => + { + var path = config.GetRequiredValue("assetStore:folder:path"); + + services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) + .As(); + }, + ["GoogleCloud"] = () => + { + var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); + + services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) + .As(); + }, + ["AzureBlob"] = () => + { + var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"); + var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); + + services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) + .As(); + }, + ["MongoDb"] = () => + { + var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); + var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); + + services.AddSingletonAs(c => + { + var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); + + var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions + { + BucketName = mongoGridFsBucketName + }); + + return new MongoGridFsAssetStore(gridFsbucket); + }) + .As(); + }, + ["Ftp"] = () => + { + var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); + var serverPort = config.GetOptionalValue("assetStore:ftp:serverPort", 21); + + var username = config.GetRequiredValue("assetStore:ftp:username"); + var password = config.GetRequiredValue("assetStore:ftp:password"); + + var path = config.GetOptionalValue("assetStore:ftp:path", "/"); + + services.AddSingletonAs(c => + { + var factory = new Func(() => new FtpClient(serverHost, serverPort, username, password)); + + return new FTPAssetStore(factory, path, c.GetRequiredService()); + }) + .As(); + } + }); + + services.AddSingletonAs() + .As(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs new file mode 100644 index 000000000..fe2edb927 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Config.Domain +{ + public static class BackupsServices + { + public static void AddSquidexBackups(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs new file mode 100644 index 000000000..91526ff5d --- /dev/null +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Apps.Invitation; +using Squidex.Domain.Apps.Entities.Apps.Templates; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Comments; +using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Web.CommandMiddlewares; + +namespace Squidex.Config.Domain +{ + public static class CommandsServices + { + public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("mode")); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingleton(typeof(IEventEnricher<>), typeof(SquidexEventEnricher<>)); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/CommentsServices.cs b/backend/src/Squidex/Config/Domain/CommentsServices.cs new file mode 100644 index 000000000..2d3141a8c --- /dev/null +++ b/backend/src/Squidex/Config/Domain/CommentsServices.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Comments; + +namespace Squidex.Config.Domain +{ + public static class CommentsServices + { + public static void AddSquidexComments(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs new file mode 100644 index 000000000..d546c4820 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Squidex.Config.Domain +{ + public static class ConfigurationExtensions + { + public static void ConfigureForSquidex(this IConfigurationBuilder builder) + { + builder.AddJsonFile($"appsettings.Custom.json", true); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs new file mode 100644 index 000000000..381f9e496 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Queries; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Config.Domain +{ + public static class ContentsServices + { + public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("contents")); + + services.AddSingletonAs(c => new Lazy(() => c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs>() + .AsSelf(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/EventPublishersServices.cs b/backend/src/Squidex/Config/Domain/EventPublishersServices.cs new file mode 100644 index 000000000..d1dfbd662 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/EventPublishersServices.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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; + +namespace Squidex.Config.Domain +{ + public static class EventPublishersServices + { + public static void AddSquidexEventPublisher(this IServiceCollection services, IConfiguration config) + { + var eventPublishers = config.GetSection("eventPublishers"); + + foreach (var child in eventPublishers.GetChildren()) + { + var eventPublisherType = child.GetValue("type"); + + if (string.IsNullOrWhiteSpace(eventPublisherType)) + { + throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'."); + } + + var eventsFilter = child.GetValue("eventsFilter"); + + var enabled = child.GetValue("enabled"); + + if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) + { + var publisherConfig = child.GetValue("configuration"); + + if (string.IsNullOrWhiteSpace(publisherConfig)) + { + throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'."); + } + + var exchange = child.GetValue("exchange"); + + if (string.IsNullOrWhiteSpace(exchange)) + { + throw new ConfigurationException($"Configure EventPublisher RabbitMq exchange with 'eventPublishers:{child.Key}:configuration'."); + } + + var name = $"EventPublishers_{child.Key}"; + + if (enabled) + { + services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) + .As(); + } + } + else + { + throw new ConfigurationException($"Unsupported value '{child.Key}' for 'eventPublishers:{child.Key}:type', supported: RabbitMq."); + } + } + } + } +} diff --git a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs new file mode 100644 index 000000000..d35566e93 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using EventStore.ClientAPI; +using Microsoft.Azure.Documents.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.States; + +namespace Squidex.Config.Domain +{ + public static class EventSourcingServices + { + public static void AddSquidexEventSourcing(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("eventStore:type", new Alternatives + { + ["MongoDb"] = () => + { + var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database"); + + services.AddSingletonAs(c => + { + var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongDatabase = mongoClient.GetDatabase(mongoDatabaseName); + + return new MongoEventStore(mongDatabase, c.GetRequiredService()); + }) + .As(); + }, + ["CosmosDb"] = () => + { + var cosmosDbConfiguration = config.GetRequiredValue("eventStore:cosmosDB:configuration"); + var cosmosDbMasterKey = config.GetRequiredValue("eventStore:cosmosDB:masterKey"); + var cosmosDbDatabase = config.GetRequiredValue("eventStore:cosmosDB:database"); + + services.AddSingletonAs(c => new DocumentClient(new Uri(cosmosDbConfiguration), cosmosDbMasterKey, c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs(c => new CosmosDbEventStore( + c.GetRequiredService(), + cosmosDbMasterKey, + cosmosDbDatabase, + c.GetRequiredService())) + .As(); + + services.AddHealthChecks() + .AddCheck("CosmosDB", tags: new[] { "node" }); + }, + ["GetEventStore"] = () => + { + var eventStoreConfiguration = config.GetRequiredValue("eventStore:getEventStore:configuration"); + var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost"); + var eventStorePrefix = config.GetValue("eventStore:getEventStore:prefix"); + + services.AddSingletonAs(_ => EventStoreConnection.Create(eventStoreConfiguration)) + .As(); + + services.AddSingletonAs(c => new GetEventStore( + c.GetRequiredService(), + c.GetRequiredService(), + eventStorePrefix, + eventStoreProjectionHost)) + .As(); + + services.AddHealthChecks() + .AddCheck("EventStore", tags: new[] { "node" }); + } + }); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs(c => + { + var allEventConsumers = c.GetServices(); + + return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); + }); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs new file mode 100644 index 000000000..5e92bc0d5 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Apps.Diagnostics; +using Squidex.Infrastructure.Diagnostics; + +namespace Squidex.Config.Domain +{ + public static class HealthCheckServices + { + public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("healthz:gc")); + + services.AddHealthChecks() + .AddCheck("GC", tags: new[] { "node" }) + .AddCheck("Orleans", tags: new[] { "cluster" }) + .AddCheck("Orleans App", tags: new[] { "cluster" }); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/HistoryServices.cs b/backend/src/Squidex/Config/Domain/HistoryServices.cs new file mode 100644 index 000000000..3582407f5 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/HistoryServices.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Config.Domain +{ + public static class HistoryServices + { + public static void AddSquidexHistory(this IServiceCollection services) + { + services.AddSingletonAs() + .As().As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs new file mode 100644 index 000000000..4c309c4a6 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Orleans; +using Squidex.Areas.Api.Controllers.Contents; +using Squidex.Areas.Api.Controllers.Contents.Generator; +using Squidex.Areas.Api.Controllers.News; +using Squidex.Areas.Api.Controllers.News.Service; +using Squidex.Areas.Api.Controllers.UI; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Pipeline.Robots; +using Squidex.Web; +using Squidex.Web.Pipeline; + +#pragma warning disable RECS0092 // Convert field to readonly + +namespace Squidex.Config.Domain +{ + public static class InfrastructureServices + { + public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("urls")); + services.Configure( + config.GetSection("exposedConfiguration")); + + services.AddSingletonAs(_ => SystemClock.Instance) + .As(); + + services.AddSingletonAs>() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingleton>(DomainObjectGrainFormatter.Format); + } + + public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("usage")); + + services.AddSingletonAs(c => new CachingUsageTracker( + c.GetRequiredService(), + c.GetRequiredService())) + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs>() + .AsSelf(); + } + + public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("translations:deepL")); + services.Configure( + config.GetSection("languages")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + } + + public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("robots")); + services.Configure( + config.GetSection("etags")); + services.Configure( + config.GetSection("contentsController")); + services.Configure( + config.GetSection("ui")); + services.Configure( + config.GetSection("news")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs new file mode 100644 index 000000000..8f7a1cdad --- /dev/null +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Log.Adapter; +using Squidex.Web.Pipeline; + +namespace Squidex.Config.Domain +{ + public static class LoggingServices + { + public static void ConfigureForSquidex(this ILoggingBuilder builder, IConfiguration config) + { + builder.ClearProviders(); + + builder.AddConfiguration(config.GetSection("logging")); + + builder.AddSemanticLog(); + builder.AddFilters(); + + builder.Services.AddServices(config); + } + + private static void AddServices(this IServiceCollection services, IConfiguration config) + { + if (config.GetValue("logging:human")) + { + services.AddSingletonAs(_ => JsonLogWriterFactory.Readable()) + .As(); + } + else + { + services.AddSingletonAs(_ => JsonLogWriterFactory.Default()) + .As(); + } + + var loggingFile = config.GetValue("logging:file"); + + if (!string.IsNullOrWhiteSpace(loggingFile)) + { + services.AddSingletonAs(_ => new FileChannel(loggingFile)) + .As(); + } + + var useColors = config.GetValue("logging:colors"); + + services.AddSingletonAs(_ => new ConsoleLogChannel(useColors)) + .As(); + + services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid())) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + } + + private static void AddFilters(this ILoggingBuilder builder) + { + builder.AddFilter((category, level) => + { + if (level < LogLevel.Information) + { + return false; + } + + if (category.StartsWith("Orleans.Runtime.NoOpHostEnvironmentStatistics", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Error; + } + + if (category.StartsWith("Orleans.Runtime.SafeTimer", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Error; + } + + if (category.StartsWith("Orleans.Runtime.Scheduler", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Orleans.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } +#if LOG_ALL_IDENTITY_SERVER + if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } +#endif + return true; + }); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/MigrationServices.cs b/backend/src/Squidex/Config/Domain/MigrationServices.cs new file mode 100644 index 000000000..e70b38dcf --- /dev/null +++ b/backend/src/Squidex/Config/Domain/MigrationServices.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01; +using Migrate_01.Migrations; +using Squidex.Infrastructure.Migrations; + +namespace Squidex.Config.Domain +{ + public static class MigrationServices + { + public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("rebuild")); + + services.AddSingletonAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/NotificationsServices.cs b/backend/src/Squidex/Config/Domain/NotificationsServices.cs new file mode 100644 index 000000000..951eaa4a1 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/NotificationsServices.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.History.Notifications; +using Squidex.Infrastructure.Email; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Config.Domain +{ + public static class NotificationsServices + { + public static void AddSquidexNotifications(this IServiceCollection services, IConfiguration config) + { + var emailOptions = config.GetSection("email:smtp").Get(); + + if (emailOptions.IsConfigured()) + { + services.AddSingleton(Options.Create(emailOptions)); + + services.Configure( + config.GetSection("email:notifications")); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + } + else + { + services.AddSingletonAs() + .AsOptional(); + } + + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs new file mode 100644 index 000000000..f0c904be2 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL; +using GraphQL.DataLoader; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Infrastructure.Assets; +using Squidex.Web; +using Squidex.Web.Services; + +namespace Squidex.Config.Domain +{ + public static class QueryServices + { + public static void AddSquidexQueries(this IServiceCollection services, IConfiguration config) + { + var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); + + services.AddSingletonAs(c => new UrlGenerator( + c.GetRequiredService>(), + c.GetRequiredService(), + exposeSourceUrl)) + .As().As().As().As(); + + services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t))) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs new file mode 100644 index 000000000..2cfc9c81a --- /dev/null +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Queries; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Config.Domain +{ + public static class RuleServices + { + public static void AddSquidexRules(this IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("rules")); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As().As(); + + services.AddSingletonAs() + .As().AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs>() + .AsSelf(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/SchemasServices.cs b/backend/src/Squidex/Config/Domain/SchemasServices.cs new file mode 100644 index 000000000..9326df263 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/SchemasServices.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Config.Domain +{ + public static class SchemasServices + { + public static void AddSquidexSchemas(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + } + } +} \ No newline at end of file diff --git a/src/Squidex/Config/Domain/SerializationInitializer.cs b/backend/src/Squidex/Config/Domain/SerializationInitializer.cs similarity index 100% rename from src/Squidex/Config/Domain/SerializationInitializer.cs rename to backend/src/Squidex/Config/Domain/SerializationInitializer.cs diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs new file mode 100644 index 000000000..dbd3e7c0d --- /dev/null +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Migrate_01; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Contents.Json; +using Squidex.Domain.Apps.Core.Rules.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Config.Domain +{ + public static class SerializationServices + { + private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) + { + settings.Converters.Add(new StringEnumConverter()); + + settings.ContractResolver = new ConverterContractResolver( + new AppClientsConverter(), + new AppContributorsConverter(), + new AppPatternsConverter(), + new ClaimsPrincipalConverter(), + new ContentFieldDataConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new InstantConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new LanguagesConfigConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new RolesConverter(), + new RuleConverter(), + new SchemaConverter(), + new StatusConverter(), + new StringEnumConverter(), + new WorkflowConverter(), + new WorkflowTransitionConverter()); + + settings.NullValueHandling = NullValueHandling.Ignore; + + settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; + settings.DateParseHandling = DateParseHandling.None; + + settings.TypeNameHandling = typeNameHandling; + + return settings; + } + + public static IServiceCollection AddSquidexSerializers(this IServiceCollection services) + { + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs(c => JsonSerializer.Create(c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs(c => + { + var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); + + var typeNameRegistry = c.GetService(); + + if (typeNameRegistry != null) + { + serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); + } + + return serializerSettings; + }).As(); + + return services; + } + + public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder mvc) + { + mvc.AddNewtonsoftJson(options => + { + ConfigureJson(options.SerializerSettings, TypeNameHandling.None); + }); + + return mvc; + } + } +} diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs new file mode 100644 index 000000000..c216fd390 --- /dev/null +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01.Migrations.MongoDb; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Assets; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.History; +using Squidex.Domain.Apps.Entities.MongoDb.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Users; +using Squidex.Domain.Users.MongoDb; +using Squidex.Domain.Users.MongoDb.Infrastructure; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Config.Domain +{ + public static class StoreServices + { + public static void AddSquidexStoreServices(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("store:type", new Alternatives + { + ["MongoDB"] = () => + { + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); + + services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); + + services.AddSingletonAs(_ => Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s))) + .As(); + + services.AddSingletonAs(c => c.GetRequiredService().GetDatabase(mongoDatabaseName)) + .As(); + + services.AddTransientAs(c => new DeleteContentCollections(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) + .As(); + + services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddHealthChecks() + .AddCheck("MongoDB", tags: new[] { "node" }); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As>(); + + services.AddSingletonAs() + .As>() + .As(); + + services.AddSingletonAs() + .As() + .As>(); + + services.AddSingletonAs(c => new MongoContentRepository( + c.GetRequiredService().GetDatabase(mongoContentDatabaseName), + c.GetRequiredService(), + c.GetRequiredService(), + c.GetRequiredService(), + c.GetRequiredService())) + .As() + .As>() + .As(); + + var registration = services.FirstOrDefault(x => x.ServiceType == typeof(IPersistedGrantStore)); + + if (registration == null || registration.ImplementationType == typeof(InMemoryPersistedGrantStore)) + { + services.AddSingletonAs() + .As(); + } + } + }); + + services.AddSingleton(typeof(IStore<>), typeof(Store<>)); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs new file mode 100644 index 000000000..bfda6b0da --- /dev/null +++ b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Web; + +namespace Squidex.Config.Domain +{ + public static class SubscriptionServices + { + public static void AddSquidexSubscriptions(this IServiceCollection services, IConfiguration config) + { + services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()!); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + + services.AddSingletonAs() + .AsOptional(); + } + } +} diff --git a/src/Squidex/Config/MyIdentityOptions.cs b/backend/src/Squidex/Config/MyIdentityOptions.cs similarity index 100% rename from src/Squidex/Config/MyIdentityOptions.cs rename to backend/src/Squidex/Config/MyIdentityOptions.cs diff --git a/src/Squidex/Config/Orleans/Helper.cs b/backend/src/Squidex/Config/Orleans/Helper.cs similarity index 100% rename from src/Squidex/Config/Orleans/Helper.cs rename to backend/src/Squidex/Config/Orleans/Helper.cs diff --git a/backend/src/Squidex/Config/Orleans/OrleansServices.cs b/backend/src/Squidex/Config/Orleans/OrleansServices.cs new file mode 100644 index 000000000..7f320a676 --- /dev/null +++ b/backend/src/Squidex/Config/Orleans/OrleansServices.cs @@ -0,0 +1,125 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Orleans; +using Orleans.Configuration; +using Orleans.Hosting; +using Orleans.Providers.MongoDB.Configuration; +using OrleansDashboard; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Web; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Squidex.Config.Orleans +{ + public static class OrleansServices + { + public static void ConfigureForSquidex(this ISiloBuilder builder, IConfiguration config) + { + builder.ConfigureServices(siloServices => + { + siloServices.AddSingleton(); + siloServices.AddScoped(); + + siloServices.AddScoped(typeof(IGrainState<>), typeof(Squidex.Infrastructure.Orleans.GrainState<>)); + }); + + builder.ConfigureApplicationParts(parts => + { + parts.AddApplicationPart(SquidexEntities.Assembly); + parts.AddApplicationPart(SquidexInfrastructure.Assembly); + }); + + builder.Configure(options => + { + options.Configure(); + }); + + builder.Configure(options => + { + options.FastKillOnProcessExit = false; + }); + + builder.Configure(options => + { + options.HideTrace = true; + }); + + builder.UseDashboard(options => + { + options.HostSelf = false; + }); + + builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); + + var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); + var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000); + + var address = Helper.ResolveIPAddressAsync(Dns.GetHostName(), AddressFamily.InterNetwork).Result; + + builder.ConfigureEndpoints( + address, + orleansPortSilo, + orleansPortGateway, + true); + + config.ConfigureByOption("orleans:clustering", new Alternatives + { + ["MongoDB"] = () => + { + builder.UseMongoDBClustering(options => + { + options.Configure(config); + }); + }, + ["Development"] = () => + { + builder.UseDevelopmentClustering(new IPEndPoint(address, orleansPortSilo)); + } + }); + + config.ConfigureByOption("store:type", new Alternatives + { + ["MongoDB"] = () => + { + builder.UseMongoDBReminders(options => + { + options.Configure(config); + }); + } + }); + } + + private static void Configure(this MongoDBOptions options, IConfiguration config) + { + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + + options.ConnectionString = mongoConfiguration; + options.CollectionPrefix = "Orleans_"; + + options.DatabaseName = mongoDatabaseName; + } + + private static void Configure(this ClusterOptions options) + { + options.ClusterId = Constants.OrleansClusterId; + options.ServiceId = Constants.OrleansClusterId; + } + } +} diff --git a/backend/src/Squidex/Config/Startup/BackgroundHost.cs b/backend/src/Squidex/Config/Startup/BackgroundHost.cs new file mode 100644 index 000000000..deb6c41fe --- /dev/null +++ b/backend/src/Squidex/Config/Startup/BackgroundHost.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. + +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class BackgroundHost : SafeHostedService + { + private readonly IEnumerable targets; + + public BackgroundHost(IEnumerable targets, ISemanticLog log) + : base(log) + { + this.targets = targets; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + foreach (var target in targets.Distinct()) + { + await target.StartAsync(ct); + + log.LogInformation(w => w.WriteProperty("backgroundSystem", target.ToString())); + } + } + } +} diff --git a/backend/src/Squidex/Config/Startup/InitializerHost.cs b/backend/src/Squidex/Config/Startup/InitializerHost.cs new file mode 100644 index 000000000..812fd0f19 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/InitializerHost.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class InitializerHost : SafeHostedService + { + private readonly IEnumerable targets; + + public InitializerHost(IEnumerable targets, ISemanticLog log) + : base(log) + { + this.targets = targets; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + foreach (var target in targets.Distinct()) + { + await target.InitializeAsync(ct); + + log.LogInformation(w => w.WriteProperty("initializedSystem", target.GetType().Name)); + } + } + } +} diff --git a/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs new file mode 100644 index 000000000..6996f1435 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Config.Startup +{ + public sealed class LogConfigurationHost : SafeHostedService + { + private readonly IConfiguration configuration; + + public LogConfigurationHost(ISemanticLog log, IConfiguration configuration) + : base(log) + { + this.configuration = configuration; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + log.LogInformation(w => w + .WriteProperty("message", "Application started") + .WriteObject("environment", c => + { + var logged = new HashSet(StringComparer.OrdinalIgnoreCase); + + var orderedConfigs = configuration.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); + + foreach (var (key, val) in orderedConfigs) + { + if (logged.Add(key)) + { + c.WriteProperty(key.ToLowerInvariant(), val); + } + } + })); + + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs new file mode 100644 index 000000000..5523ca278 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Migrate_01; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class MigrationRebuilderHost : SafeHostedService + { + private readonly RebuildRunner rebuildRunner; + + public MigrationRebuilderHost(RebuildRunner rebuildRunner, ISemanticLog log) + : base(log) + { + this.rebuildRunner = rebuildRunner; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + return rebuildRunner.RunAsync(ct); + } + } +} diff --git a/backend/src/Squidex/Config/Startup/MigratorHost.cs b/backend/src/Squidex/Config/Startup/MigratorHost.cs new file mode 100644 index 000000000..29fcf7cb1 --- /dev/null +++ b/backend/src/Squidex/Config/Startup/MigratorHost.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Migrations; + +namespace Squidex.Config.Startup +{ + public sealed class MigratorHost : SafeHostedService + { + private readonly Migrator migrator; + + public MigratorHost(Migrator migrator, ISemanticLog log) + : base(log) + { + this.migrator = migrator; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + return migrator.MigrateAsync(ct); + } + } +} diff --git a/backend/src/Squidex/Config/Startup/SafeHostedService.cs b/backend/src/Squidex/Config/Startup/SafeHostedService.cs new file mode 100644 index 000000000..286d22dbf --- /dev/null +++ b/backend/src/Squidex/Config/Startup/SafeHostedService.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Config.Startup +{ + public abstract class SafeHostedService : IHostedService + { + private readonly ISemanticLog log; + private bool isStarted; + + protected SafeHostedService(ISemanticLog log) + { + this.log = log; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await StartAsync(log, cancellationToken); + + isStarted = true; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (isStarted) + { + await StopAsync(log, cancellationToken); + } + } + + protected abstract Task StartAsync(ISemanticLog log, CancellationToken ct); + + protected virtual Task StopAsync(ISemanticLog log, CancellationToken ct) + { + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs new file mode 100644 index 000000000..3bc5022cf --- /dev/null +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -0,0 +1,121 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Net.Http.Headers; +using Squidex.Infrastructure.Json; +using Squidex.Pipeline.Robots; +using Squidex.Web.Pipeline; + +namespace Squidex.Config.Web +{ + public static class WebExtensions + { + public static IApplicationBuilder UseSquidexLocalCache(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } + + public static IApplicationBuilder UseSquidexTracking(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } + + public static IApplicationBuilder UseSquidexHealthCheck(this IApplicationBuilder app) + { + var serializer = app.ApplicationServices.GetRequiredService(); + + var writer = new Func((httpContext, report) => + { + var response = new + { + Entries = report.Entries.ToDictionary(x => x.Key, x => + { + var value = x.Value; + + return new + { + Data = value.Data.Count > 0 ? new Dictionary(value.Data) : null, + value.Description, + value.Duration, + value.Status + }; + }), + report.Status, + report.TotalDuration + }; + + var json = serializer.Serialize(response); + + httpContext.Response.Headers[HeaderNames.ContentType] = "text/json"; + + return httpContext.Response.WriteAsync(json); + }); + + app.UseHealthChecks("/readiness", new HealthCheckOptions + { + Predicate = check => true, + ResponseWriter = writer + }); + + app.UseHealthChecks("/healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("node"), + ResponseWriter = writer + }); + + app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("cluster"), + ResponseWriter = writer + }); + + return app; + } + + public static IApplicationBuilder UseSquidexRobotsTxt(this IApplicationBuilder app) + { + app.Map("/robots.txt", builder => builder.UseMiddleware()); + + return app; + } + + public static void UseSquidexCors(this IApplicationBuilder app) + { + app.UseCors(builder => builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + } + + public static void UseSquidexForwardingRules(this IApplicationBuilder app) + { + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedProto, + ForwardLimit = null, + RequireHeaderSymmetry = false + }); + + app.UseMiddleware(); + app.UseMiddleware(); + } + } +} diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs new file mode 100644 index 000000000..b6a8b74d7 --- /dev/null +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -0,0 +1,76 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Config.Domain; +using Squidex.Domain.Apps.Entities; +using Squidex.Pipeline.Plugins; +using Squidex.Pipeline.Robots; +using Squidex.Web; +using Squidex.Web.Pipeline; + +namespace Squidex.Config.Web +{ + public static class WebServices + { + public static void AddSquidexMvcWithPlugins(this IServiceCollection services, IConfiguration config) + { + services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config, typeof(WebServices).Assembly)) + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + + services.AddMvc(options => + { + options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + }) + .AddSquidexPlugins(config) + .AddSquidexSerializers(); + } + } +} diff --git a/src/Squidex/Docs/schemabody.md b/backend/src/Squidex/Docs/schemabody.md similarity index 100% rename from src/Squidex/Docs/schemabody.md rename to backend/src/Squidex/Docs/schemabody.md diff --git a/src/Squidex/Docs/schemaquery.md b/backend/src/Squidex/Docs/schemaquery.md similarity index 100% rename from src/Squidex/Docs/schemaquery.md rename to backend/src/Squidex/Docs/schemaquery.md diff --git a/src/Squidex/Docs/security.md b/backend/src/Squidex/Docs/security.md similarity index 100% rename from src/Squidex/Docs/security.md rename to backend/src/Squidex/Docs/security.md diff --git a/backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs b/backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs new file mode 100644 index 000000000..140bb62d1 --- /dev/null +++ b/backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// 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.IO; +using Microsoft.AspNetCore.Http; +using NJsonSchema; +using NSwag; + +namespace Squidex.Pipeline.OpenApi +{ + public static class NSwagHelper + { + public static readonly string SecurityDocs = LoadDocs("security"); + + public static readonly string SchemaBodyDocs = LoadDocs("schemabody"); + + public static readonly string SchemaQueryDocs = LoadDocs("schemaquery"); + + private static string LoadDocs(string name) + { + var assembly = typeof(NSwagHelper).Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) + { + using (var streamReader = new StreamReader(resourceStream!)) + { + return streamReader.ReadToEnd(); + } + } + } + + public static OpenApiDocument CreateApiDocument(HttpContext context, string appName) + { + var scheme = + string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? + OpenApiSchema.Http : + OpenApiSchema.Https; + + var document = new OpenApiDocument + { + Schemes = new List + { + scheme + }, + Consumes = new List + { + "application/json" + }, + Produces = new List + { + "application/json" + }, + Info = new OpenApiInfo + { + Title = $"Squidex API for {appName} App" + }, + SchemaType = SchemaType.OpenApi3 + }; + + if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) + { + document.Host = context.Request.Host.Value; + } + + return document; + } + + public static void AddQuery(this OpenApiOperation operation, string name, JsonObjectType type, string description) + { + var schema = new JsonSchema { Type = type }; + + operation.AddParameter(name, schema, OpenApiParameterKind.Query, description, false); + } + + public static void AddPathParameter(this OpenApiOperation operation, string name, JsonObjectType type, string description, string? format = null) + { + var schema = new JsonSchema { Type = type, Format = format }; + + operation.AddParameter(name, schema, OpenApiParameterKind.Path, description, true); + } + + public static void AddBody(this OpenApiOperation operation, string name, JsonSchema schema, string description) + { + operation.AddParameter(name, schema, OpenApiParameterKind.Body, description, true); + } + + private static void AddParameter(this OpenApiOperation operation, string name, JsonSchema schema, OpenApiParameterKind kind, string description, bool isRequired) + { + var parameter = new OpenApiParameter { Schema = schema, Name = name, Kind = kind }; + + if (!string.IsNullOrWhiteSpace(description)) + { + parameter.Description = description; + } + + parameter.IsRequired = isRequired; + + operation.Parameters.Add(parameter); + } + + public static void AddResponse(this OpenApiOperation operation, string statusCode, string description, JsonSchema? schema = null) + { + var response = new OpenApiResponse { Description = description, Schema = schema }; + + operation.Responses.Add(statusCode, response); + } + } +} diff --git a/src/Squidex/Pipeline/Plugins/MvcParts.cs b/backend/src/Squidex/Pipeline/Plugins/MvcParts.cs similarity index 100% rename from src/Squidex/Pipeline/Plugins/MvcParts.cs rename to backend/src/Squidex/Pipeline/Plugins/MvcParts.cs diff --git a/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs b/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs new file mode 100644 index 000000000..cab52c7fa --- /dev/null +++ b/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Pipeline.Plugins +{ + public static class PluginExtensions + { + public static IMvcBuilder AddSquidexPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) + { + var pluginManager = new PluginManager(); + + var options = config.Get(); + + if (options.Plugins != null) + { + foreach (var path in options.Plugins) + { + var plugin = PluginLoaders.LoadPlugin(path); + + if (plugin != null) + { + try + { + var pluginAssembly = plugin.LoadDefaultAssembly(); + + pluginAssembly.AddParts(mvcBuilder); + pluginManager.Add(path, pluginAssembly); + } + catch (Exception ex) + { + pluginManager.LogException(path, "LoadingAssembly", ex); + } + } + else + { + pluginManager.LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); + } + } + } + + pluginManager.ConfigureServices(mvcBuilder.Services, config); + + mvcBuilder.Services.AddSingleton(pluginManager); + + return mvcBuilder; + } + + public static void UsePluginsBefore(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.ConfigureBefore(app); + } + + public static void UsePluginsAfter(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.ConfigureAfter(app); + } + + public static void UsePlugins(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.Log(app.ApplicationServices.GetService()); + } + } +} diff --git a/backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs b/backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs new file mode 100644 index 000000000..d2471144f --- /dev/null +++ b/backend/src/Squidex/Pipeline/Plugins/PluginLoaders.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using McMaster.NETCore.Plugins; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Plugins; +using Squidex.Web; + +namespace Squidex.Pipeline.Plugins +{ + public static class PluginLoaders + { + private static readonly Type[] SharedTypes = + { + typeof(IPlugin), + typeof(SquidexCoreModel), + typeof(SquidexCoreOperations), + typeof(SquidexEntities), + typeof(SquidexEvents), + typeof(SquidexInfrastructure), + typeof(SquidexWeb) + }; + + public static PluginLoader? LoadPlugin(string pluginPath) + { + foreach (var candidate in GetPaths(pluginPath)) + { + if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) + { + return PluginLoader.CreateFromAssemblyFile(candidate.FullName, config => + { + config.PreferSharedTypes = true; + + config.SharedAssemblies.AddRange(SharedTypes.Select(x => x.Assembly.GetName())); + }); + } + } + + return null; + } + + private static IEnumerable GetPaths(string pluginPath) + { + var candidate = new FileInfo(Path.GetFullPath(pluginPath)); + + if (candidate.Exists) + { + yield return candidate; + } + + if (!Path.IsPathRooted(pluginPath)) + { + var assembly = Assembly.GetEntryAssembly(); + + if (assembly != null) + { + var directory = Path.GetDirectoryName(assembly.Location); + + if (directory != null) + { + candidate = new FileInfo(Path.Combine(directory, pluginPath)); + + if (candidate.Exists) + { + yield return candidate; + } + } + } + } + } + } +} diff --git a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs new file mode 100644 index 000000000..a75e32ded --- /dev/null +++ b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; + +namespace Squidex.Pipeline.Robots +{ + public sealed class RobotsTxtMiddleware : IMiddleware + { + private readonly RobotsTxtOptions robotsTxtOptions; + + public RobotsTxtMiddleware(IOptions robotsTxtOptions) + { + Guard.NotNull(robotsTxtOptions); + + this.robotsTxtOptions = robotsTxtOptions.Value; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(robotsTxtOptions.Text)) + { + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = 200; + + await context.Response.WriteAsync(robotsTxtOptions.Text); + } + else + { + await next(context); + } + } + + private static bool CanServeRequest(HttpRequest request) + { + return HttpMethods.IsGet(request.Method) && string.IsNullOrEmpty(request.Path); + } + } +} diff --git a/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs similarity index 100% rename from src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs rename to backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs diff --git a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs new file mode 100644 index 000000000..da212d8c0 --- /dev/null +++ b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Squidex.Pipeline.Squid +{ + public sealed class SquidMiddleware + { + private readonly RequestDelegate next; + private readonly string squidHappyLG = LoadSvg("happy"); + private readonly string squidHappySM = LoadSvg("happy-sm"); + private readonly string squidSadLG = LoadSvg("sad"); + private readonly string squidSadSM = LoadSvg("sad-sm"); + + public SquidMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task Invoke(HttpContext context) + { + var request = context.Request; + + if (request.Path.Equals("/squid.svg")) + { + var face = "sad"; + + if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) + { + face = faceValue; + } + + var isSad = face == "sad"; + + var title = isSad ? "OH DAMN!" : "OH YEAH!"; + + if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) + { + title = titleValue; + } + + var text = "text"; + + if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) + { + text = textValue; + } + + var background = isSad ? "#F5F5F9" : "#4CC159"; + + if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) + { + background = backgroundValue; + } + + var isSmall = request.Query.TryGetValue("small", out _); + + string svg; + + if (isSmall) + { + svg = isSad ? squidSadSM : squidHappySM; + } + else + { + svg = isSad ? squidSadLG : squidHappyLG; + } + + var (l1, l2, l3) = SplitText(text); + + svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); + svg = svg.Replace("{{TEXT1}}", l1); + svg = svg.Replace("{{TEXT2}}", l2); + svg = svg.Replace("{{TEXT3}}", l3); + svg = svg.Replace("[COLOR]", background); + + context.Response.StatusCode = 200; + context.Response.ContentType = "image/svg+xml"; + context.Response.Headers["Cache-Control"] = "public, max-age=604800"; + + await context.Response.WriteAsync(svg); + } + else + { + await next(context); + } + } + + private static (string, string, string) SplitText(string text) + { + var result = new List(); + + var line = new StringBuilder(); + + foreach (var word in text.Split(' ')) + { + if (line.Length + word.Length > 17 && line.Length > 0) + { + result.Add(line.ToString()); + + line.Clear(); + } + + if (line.Length > 0) + { + line.Append(" "); + } + + line.Append(word); + } + + result.Add(line.ToString()); + + while (result.Count < 3) + { + result.Add(string.Empty); + } + + return (result[0], result[1], result[2]); + } + + private static string LoadSvg(string name) + { + var assembly = typeof(SquidMiddleware).Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) + { + using (var streamReader = new StreamReader(resourceStream!)) + { + return streamReader.ReadToEnd(); + } + } + } + } +} diff --git a/src/Squidex/Pipeline/Squid/icon-happy-sm.svg b/backend/src/Squidex/Pipeline/Squid/icon-happy-sm.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-happy-sm.svg rename to backend/src/Squidex/Pipeline/Squid/icon-happy-sm.svg diff --git a/src/Squidex/Pipeline/Squid/icon-happy.svg b/backend/src/Squidex/Pipeline/Squid/icon-happy.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-happy.svg rename to backend/src/Squidex/Pipeline/Squid/icon-happy.svg diff --git a/src/Squidex/Pipeline/Squid/icon-sad-sm.svg b/backend/src/Squidex/Pipeline/Squid/icon-sad-sm.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-sad-sm.svg rename to backend/src/Squidex/Pipeline/Squid/icon-sad-sm.svg diff --git a/src/Squidex/Pipeline/Squid/icon-sad.svg b/backend/src/Squidex/Pipeline/Squid/icon-sad.svg similarity index 100% rename from src/Squidex/Pipeline/Squid/icon-sad.svg rename to backend/src/Squidex/Pipeline/Squid/icon-sad.svg diff --git a/backend/src/Squidex/Program.cs b/backend/src/Squidex/Program.cs new file mode 100644 index 000000000..791e13cbb --- /dev/null +++ b/backend/src/Squidex/Program.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Config.Domain; +using Squidex.Config.Orleans; +using Squidex.Config.Startup; + +namespace Squidex +{ + public static class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureLogging((context, builder) => + { + builder.ConfigureForSquidex(context.Configuration); + }) + .ConfigureAppConfiguration((hostContext, builder) => + { + builder.ConfigureForSquidex(); + }) + .ConfigureServices(services => + { + // Step 0: Log all configuration. + services.AddHostedService(); + + // Step 1: Initialize all services. + services.AddHostedService(); + + // Step 2: Create admin user. + services.AddHostedService(); + }) + .UseOrleans((context, builder) => + { + // Step 3: Start Orleans. + builder.ConfigureForSquidex(context.Configuration); + }) + .ConfigureServices(services => + { + // Step 4: Run migration. + services.AddHostedService(); + + // Step 5: Run rebuild processes. + services.AddHostedService(); + + // Step 6: Start background processes. + services.AddHostedService(); + }) + .ConfigureWebHostDefaults(builder => + { + builder.UseStartup(); + }); + } +} diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj new file mode 100644 index 000000000..3ab06a94a --- /dev/null +++ b/backend/src/Squidex/Squidex.csproj @@ -0,0 +1,130 @@ + + + netcoreapp3.0 + Latest + true + 8.0 + enable + + + + full + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_DocumentationFile Include="$(DocumentationFile)" /> + + + + + + true + + + + ..\..\Squidex.ruleset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060 + + \ No newline at end of file diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs new file mode 100644 index 000000000..1e9cb14d7 --- /dev/null +++ b/backend/src/Squidex/Startup.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Areas.Api; +using Squidex.Areas.Api.Config.OpenApi; +using Squidex.Areas.Frontend; +using Squidex.Areas.IdentityServer; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Areas.OrleansDashboard; +using Squidex.Areas.Portal; +using Squidex.Config.Authentication; +using Squidex.Config.Domain; +using Squidex.Config.Web; +using Squidex.Pipeline.Plugins; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Squidex +{ + public sealed class Startup + { + private readonly IConfiguration config; + + public Startup(IConfiguration config) + { + this.config = config; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpClient(); + services.AddMemoryCache(); + + services.AddSquidexMvcWithPlugins(config); + + services.AddSquidexApps(); + services.AddSquidexAssetInfrastructure(config); + services.AddSquidexAssets(config); + services.AddSquidexAuthentication(config); + services.AddSquidexBackups(); + services.AddSquidexCommands(config); + services.AddSquidexComments(); + services.AddSquidexContents(config); + services.AddSquidexControllerServices(config); + services.AddSquidexEventPublisher(config); + services.AddSquidexEventSourcing(config); + services.AddSquidexHealthChecks(config); + services.AddSquidexHistory(); + services.AddSquidexIdentity(config); + services.AddSquidexIdentityServer(); + services.AddSquidexInfrastructure(config); + services.AddSquidexMigration(config); + services.AddSquidexNotifications(config); + services.AddSquidexOpenApiSettings(); + services.AddSquidexQueries(config); + services.AddSquidexRules(config); + services.AddSquidexSchemas(); + services.AddSquidexSerializers(); + services.AddSquidexStoreServices(config); + services.AddSquidexSubscriptions(config); + services.AddSquidexTranslation(config); + services.AddSquidexUsageTracking(config); + } + + public void Configure(IApplicationBuilder app) + { + app.UsePluginsBefore(); + + app.UseSquidexHealthCheck(); + app.UseSquidexRobotsTxt(); + app.UseSquidexTracking(); + app.UseSquidexLocalCache(); + app.UseSquidexCors(); + app.UseSquidexForwardingRules(); + + app.ConfigureApi(); + app.ConfigurePortal(); + app.ConfigureOrleansDashboard(); + app.ConfigureIdentityServer(); + app.ConfigureFrontend(); + + app.UsePluginsAfter(); + app.UsePlugins(); + } + } +} diff --git a/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json similarity index 100% rename from src/Squidex/appsettings.json rename to backend/src/Squidex/appsettings.json diff --git a/src/Squidex/wwwroot/client-callback-popup.html b/backend/src/Squidex/wwwroot/client-callback-popup.html similarity index 100% rename from src/Squidex/wwwroot/client-callback-popup.html rename to backend/src/Squidex/wwwroot/client-callback-popup.html diff --git a/src/Squidex/wwwroot/client-callback-silent.html b/backend/src/Squidex/wwwroot/client-callback-silent.html similarity index 100% rename from src/Squidex/wwwroot/client-callback-silent.html rename to backend/src/Squidex/wwwroot/client-callback-silent.html diff --git a/src/Squidex/wwwroot/favicon.ico b/backend/src/Squidex/wwwroot/favicon.ico similarity index 100% rename from src/Squidex/wwwroot/favicon.ico rename to backend/src/Squidex/wwwroot/favicon.ico diff --git a/src/Squidex/wwwroot/images/add-app.png b/backend/src/Squidex/wwwroot/images/add-app.png similarity index 100% rename from src/Squidex/wwwroot/images/add-app.png rename to backend/src/Squidex/wwwroot/images/add-app.png diff --git a/src/Squidex/wwwroot/images/add-blog.png b/backend/src/Squidex/wwwroot/images/add-blog.png similarity index 100% rename from src/Squidex/wwwroot/images/add-blog.png rename to backend/src/Squidex/wwwroot/images/add-blog.png diff --git a/src/Squidex/wwwroot/images/add-identity.png b/backend/src/Squidex/wwwroot/images/add-identity.png similarity index 100% rename from src/Squidex/wwwroot/images/add-identity.png rename to backend/src/Squidex/wwwroot/images/add-identity.png diff --git a/src/Squidex/wwwroot/images/add-profile.png b/backend/src/Squidex/wwwroot/images/add-profile.png similarity index 100% rename from src/Squidex/wwwroot/images/add-profile.png rename to backend/src/Squidex/wwwroot/images/add-profile.png diff --git a/src/Squidex/wwwroot/images/asset_doc.png b/backend/src/Squidex/wwwroot/images/asset_doc.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_doc.png rename to backend/src/Squidex/wwwroot/images/asset_doc.png diff --git a/src/Squidex/wwwroot/images/asset_docx.png b/backend/src/Squidex/wwwroot/images/asset_docx.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_docx.png rename to backend/src/Squidex/wwwroot/images/asset_docx.png diff --git a/src/Squidex/wwwroot/images/asset_generic.png b/backend/src/Squidex/wwwroot/images/asset_generic.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_generic.png rename to backend/src/Squidex/wwwroot/images/asset_generic.png diff --git a/src/Squidex/wwwroot/images/asset_pdf.png b/backend/src/Squidex/wwwroot/images/asset_pdf.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_pdf.png rename to backend/src/Squidex/wwwroot/images/asset_pdf.png diff --git a/src/Squidex/wwwroot/images/asset_ppt.png b/backend/src/Squidex/wwwroot/images/asset_ppt.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_ppt.png rename to backend/src/Squidex/wwwroot/images/asset_ppt.png diff --git a/src/Squidex/wwwroot/images/asset_pptx.png b/backend/src/Squidex/wwwroot/images/asset_pptx.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_pptx.png rename to backend/src/Squidex/wwwroot/images/asset_pptx.png diff --git a/src/Squidex/wwwroot/images/asset_video.png b/backend/src/Squidex/wwwroot/images/asset_video.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_video.png rename to backend/src/Squidex/wwwroot/images/asset_video.png diff --git a/src/Squidex/wwwroot/images/asset_xls.png b/backend/src/Squidex/wwwroot/images/asset_xls.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_xls.png rename to backend/src/Squidex/wwwroot/images/asset_xls.png diff --git a/src/Squidex/wwwroot/images/asset_xlsx.png b/backend/src/Squidex/wwwroot/images/asset_xlsx.png similarity index 100% rename from src/Squidex/wwwroot/images/asset_xlsx.png rename to backend/src/Squidex/wwwroot/images/asset_xlsx.png diff --git a/src/Squidex/wwwroot/images/client.png b/backend/src/Squidex/wwwroot/images/client.png similarity index 100% rename from src/Squidex/wwwroot/images/client.png rename to backend/src/Squidex/wwwroot/images/client.png diff --git a/src/Squidex/wwwroot/images/client.svg b/backend/src/Squidex/wwwroot/images/client.svg similarity index 100% rename from src/Squidex/wwwroot/images/client.svg rename to backend/src/Squidex/wwwroot/images/client.svg diff --git a/src/Squidex/wwwroot/images/dashboard-api.png b/backend/src/Squidex/wwwroot/images/dashboard-api.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-api.png rename to backend/src/Squidex/wwwroot/images/dashboard-api.png diff --git a/src/Squidex/wwwroot/images/dashboard-feedback.png b/backend/src/Squidex/wwwroot/images/dashboard-feedback.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-feedback.png rename to backend/src/Squidex/wwwroot/images/dashboard-feedback.png diff --git a/src/Squidex/wwwroot/images/dashboard-github.png b/backend/src/Squidex/wwwroot/images/dashboard-github.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-github.png rename to backend/src/Squidex/wwwroot/images/dashboard-github.png diff --git a/src/Squidex/wwwroot/images/dashboard-schema.png b/backend/src/Squidex/wwwroot/images/dashboard-schema.png similarity index 100% rename from src/Squidex/wwwroot/images/dashboard-schema.png rename to backend/src/Squidex/wwwroot/images/dashboard-schema.png diff --git a/src/Squidex/wwwroot/images/loader-white.gif b/backend/src/Squidex/wwwroot/images/loader-white.gif similarity index 100% rename from src/Squidex/wwwroot/images/loader-white.gif rename to backend/src/Squidex/wwwroot/images/loader-white.gif diff --git a/src/Squidex/wwwroot/images/loader.gif b/backend/src/Squidex/wwwroot/images/loader.gif similarity index 100% rename from src/Squidex/wwwroot/images/loader.gif rename to backend/src/Squidex/wwwroot/images/loader.gif diff --git a/src/Squidex/wwwroot/images/login-icon.png b/backend/src/Squidex/wwwroot/images/login-icon.png similarity index 100% rename from src/Squidex/wwwroot/images/login-icon.png rename to backend/src/Squidex/wwwroot/images/login-icon.png diff --git a/src/Squidex/wwwroot/images/logo-half.png b/backend/src/Squidex/wwwroot/images/logo-half.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-half.png rename to backend/src/Squidex/wwwroot/images/logo-half.png diff --git a/src/Squidex/wwwroot/images/logo-small.png b/backend/src/Squidex/wwwroot/images/logo-small.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-small.png rename to backend/src/Squidex/wwwroot/images/logo-small.png diff --git a/src/Squidex/wwwroot/images/logo-squared-120.png b/backend/src/Squidex/wwwroot/images/logo-squared-120.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-squared-120.png rename to backend/src/Squidex/wwwroot/images/logo-squared-120.png diff --git a/src/Squidex/wwwroot/images/logo-white-small.png b/backend/src/Squidex/wwwroot/images/logo-white-small.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-white-small.png rename to backend/src/Squidex/wwwroot/images/logo-white-small.png diff --git a/src/Squidex/wwwroot/images/logo-white.png b/backend/src/Squidex/wwwroot/images/logo-white.png similarity index 100% rename from src/Squidex/wwwroot/images/logo-white.png rename to backend/src/Squidex/wwwroot/images/logo-white.png diff --git a/src/Squidex/wwwroot/images/logo.png b/backend/src/Squidex/wwwroot/images/logo.png similarity index 100% rename from src/Squidex/wwwroot/images/logo.png rename to backend/src/Squidex/wwwroot/images/logo.png diff --git a/src/Squidex/wwwroot/images/onboarding-background.png b/backend/src/Squidex/wwwroot/images/onboarding-background.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-background.png rename to backend/src/Squidex/wwwroot/images/onboarding-background.png diff --git a/src/Squidex/wwwroot/images/onboarding-step1.png b/backend/src/Squidex/wwwroot/images/onboarding-step1.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step1.png rename to backend/src/Squidex/wwwroot/images/onboarding-step1.png diff --git a/src/Squidex/wwwroot/images/onboarding-step2.png b/backend/src/Squidex/wwwroot/images/onboarding-step2.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step2.png rename to backend/src/Squidex/wwwroot/images/onboarding-step2.png diff --git a/src/Squidex/wwwroot/images/onboarding-step3.png b/backend/src/Squidex/wwwroot/images/onboarding-step3.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step3.png rename to backend/src/Squidex/wwwroot/images/onboarding-step3.png diff --git a/src/Squidex/wwwroot/images/onboarding-step4.png b/backend/src/Squidex/wwwroot/images/onboarding-step4.png similarity index 100% rename from src/Squidex/wwwroot/images/onboarding-step4.png rename to backend/src/Squidex/wwwroot/images/onboarding-step4.png diff --git a/src/Squidex/wwwroot/scripts/combined-editor.html b/backend/src/Squidex/wwwroot/scripts/combined-editor.html similarity index 100% rename from src/Squidex/wwwroot/scripts/combined-editor.html rename to backend/src/Squidex/wwwroot/scripts/combined-editor.html diff --git a/src/Squidex/wwwroot/scripts/context-editor.html b/backend/src/Squidex/wwwroot/scripts/context-editor.html similarity index 100% rename from src/Squidex/wwwroot/scripts/context-editor.html rename to backend/src/Squidex/wwwroot/scripts/context-editor.html diff --git a/src/Squidex/wwwroot/scripts/editor-sdk.js b/backend/src/Squidex/wwwroot/scripts/editor-sdk.js similarity index 100% rename from src/Squidex/wwwroot/scripts/editor-sdk.js rename to backend/src/Squidex/wwwroot/scripts/editor-sdk.js diff --git a/backend/src/Squidex/wwwroot/scripts/oidc-client.min.js b/backend/src/Squidex/wwwroot/scripts/oidc-client.min.js new file mode 100644 index 000000000..348bd594c --- /dev/null +++ b/backend/src/Squidex/wwwroot/scripts/oidc-client.min.js @@ -0,0 +1,47 @@ +var Oidc=function(t){var e={};function r(n){if(e[n])return e[n].exports;var i=e[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)r.d(n,i,function(e){return t[e]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function e(){return t.default}:function e(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=152)}([function(t,e,r){var n=r(2),i=r(19),o=r(12),s=r(13),a=r(20),u=function(t,e,r){var c,f,h,l,p=t&u.F,d=t&u.G,g=t&u.S,v=t&u.P,y=t&u.B,m=d?n:g?n[e]||(n[e]={}):(n[e]||{}).prototype,_=d?i:i[e]||(i[e]={}),S=_.prototype||(_.prototype={});for(c in d&&(r=e),r)h=((f=!p&&m&&void 0!==m[c])?m:r)[c],l=y&&f?a(h,n):v&&"function"==typeof h?a(Function.call,h):h,m&&s(m,c,h,t&u.U),_[c]!=h&&o(_,c,l),v&&S[c]!=h&&(S[c]=h)};n.core=i,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},function(t,e,r){var n=r(5);t.exports=function(t){if(!n(t))throw TypeError(t+" is not an object!");return t}},function(t,e){var r=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=r)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(t,e){for(var r=0;r=4){for(var e=arguments.length,r=Array(e),n=0;n=3){for(var e=arguments.length,r=Array(e),n=0;n=2){for(var e=arguments.length,r=Array(e),n=0;n=1){for(var e=arguments.length,r=Array(e),n=0;n0?i(n(t),9007199254740991):0}},function(t,e,r){t.exports=!r(4)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e,r){var n=r(1),i=r(103),o=r(24),s=Object.defineProperty;e.f=r(8)?Object.defineProperty:function t(e,r,a){if(n(e),r=o(r,!0),n(a),i)try{return s(e,r,a)}catch(t){}if("get"in a||"set"in a)throw TypeError("Accessors not supported!");return"value"in a&&(e[r]=a.value),e}},function(t,e,r){var n=r(25);t.exports=function(t){return Object(n(t))}},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,e,r){var n=r(9),i=r(33);t.exports=r(8)?function(t,e,r){return n.f(t,e,i(1,r))}:function(t,e,r){return t[e]=r,t}},function(t,e,r){var n=r(2),i=r(12),o=r(15),s=r(34)("src"),a=r(156),u=(""+a).split("toString");r(19).inspectSource=function(t){return a.call(t)},(t.exports=function(t,e,r,a){var c="function"==typeof r;c&&(o(r,"name")||i(r,"name",e)),t[e]!==r&&(c&&(o(r,s)||i(r,s,t[e]?""+t[e]:u.join(String(e)))),t===n?t[e]=r:a?t[e]?t[e]=r:i(t,e,r):(delete t[e],i(t,e,r)))})(Function.prototype,"toString",function t(){return"function"==typeof this&&this[s]||a.call(this)})},function(t,e,r){var n=r(0),i=r(4),o=r(25),s=/"/g,a=function(t,e,r,n){var i=String(o(t)),a="<"+e;return""!==r&&(a+=" "+r+'="'+String(n).replace(s,""")+'"'),a+">"+i+""};t.exports=function(t,e){var r={};r[t]=e(a),n(n.P+n.F*i(function(){var e=""[t]('"');return e!==e.toLowerCase()||e.split('"').length>3}),"String",r)}},function(t,e){var r={}.hasOwnProperty;t.exports=function(t,e){return r.call(t,e)}},function(t,e,r){var n=r(51),i=r(25);t.exports=function(t){return n(i(t))}},function(t,e,r){var n=r(52),i=r(33),o=r(16),s=r(24),a=r(15),u=r(103),c=Object.getOwnPropertyDescriptor;e.f=r(8)?c:function t(e,r){if(e=o(e),r=s(r,!0),u)try{return c(e,r)}catch(t){}if(a(e,r))return i(!n.f.call(e,r),e[r])}},function(t,e,r){var n=r(15),i=r(10),o=r(74)("IE_PROTO"),s=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=i(t),n(t,o)?t[o]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?s:null}},function(t,e){var r=t.exports={version:"2.6.4"};"number"==typeof __e&&(__e=r)},function(t,e,r){var n=r(11);t.exports=function(t,e,r){if(n(t),void 0===e)return t;switch(r){case 1:return function(r){return t.call(e,r)};case 2:return function(r,n){return t.call(e,r,n)};case 3:return function(r,n,i){return t.call(e,r,n,i)}}return function(){return t.apply(e,arguments)}}},function(t,e){var r={}.toString;t.exports=function(t){return r.call(t).slice(8,-1)}},function(t,e){var r=Math.ceil,n=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?n:r)(t)}},function(t,e,r){"use strict";var n=r(4);t.exports=function(t,e){return!!t&&n(function(){e?t.call(null,function(){},1):t.call(null)})}},function(t,e,r){var n=r(5);t.exports=function(t,e){if(!n(t))return t;var r,i;if(e&&"function"==typeof(r=t.toString)&&!n(i=r.call(t)))return i;if("function"==typeof(r=t.valueOf)&&!n(i=r.call(t)))return i;if(!e&&"function"==typeof(r=t.toString)&&!n(i=r.call(t)))return i;throw TypeError("Can't convert object to primitive value")}},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e,r){var n=r(0),i=r(19),o=r(4);t.exports=function(t,e){var r=(i.Object||{})[t]||Object[t],s={};s[t]=e(r),n(n.S+n.F*o(function(){r(1)}),"Object",s)}},function(t,e,r){var n=r(20),i=r(51),o=r(10),s=r(7),a=r(90);t.exports=function(t,e){var r=1==t,u=2==t,c=3==t,f=4==t,h=6==t,l=5==t||h,p=e||a;return function(e,a,d){for(var g,v,y=o(e),m=i(y),_=n(a,d,3),S=s(m.length),b=0,w=r?p(e,S):u?p(e,0):void 0;S>b;b++)if((l||b in m)&&(v=_(g=m[b],b,y),t))if(r)w[b]=v;else if(v)switch(t){case 3:return!0;case 5:return g;case 6:return b;case 2:w.push(g)}else if(f)return!1;return h?-1:c||f?f:w}}},function(t,e,r){"use strict";if(r(8)){var n=r(30),i=r(2),o=r(4),s=r(0),a=r(66),u=r(98),c=r(20),f=r(40),h=r(33),l=r(12),p=r(42),d=r(22),g=r(7),v=r(131),y=r(36),m=r(24),_=r(15),S=r(46),b=r(5),w=r(10),F=r(87),E=r(37),x=r(18),A=r(38).f,k=r(89),P=r(34),C=r(6),T=r(27),R=r(56),I=r(54),O=r(92),D=r(48),N=r(61),L=r(39),M=r(91),j=r(120),U=r(9),B=r(17),H=U.f,V=B.f,K=i.RangeError,q=i.TypeError,W=i.Uint8Array,J=Array.prototype,z=u.ArrayBuffer,Y=u.DataView,G=T(0),X=T(2),$=T(3),Q=T(4),Z=T(5),tt=T(6),et=R(!0),rt=R(!1),nt=O.values,it=O.keys,ot=O.entries,st=J.lastIndexOf,at=J.reduce,ut=J.reduceRight,ct=J.join,ft=J.sort,ht=J.slice,lt=J.toString,pt=J.toLocaleString,dt=C("iterator"),gt=C("toStringTag"),vt=P("typed_constructor"),yt=P("def_constructor"),mt=a.CONSTR,_t=a.TYPED,St=a.VIEW,bt=T(1,function(t,e){return At(I(t,t[yt]),e)}),wt=o(function(){return 1===new W(new Uint16Array([1]).buffer)[0]}),Ft=!!W&&!!W.prototype.set&&o(function(){new W(1).set({})}),Et=function(t,e){var r=d(t);if(r<0||r%e)throw K("Wrong offset!");return r},xt=function(t){if(b(t)&&_t in t)return t;throw q(t+" is not a typed array!")},At=function(t,e){if(!(b(t)&&vt in t))throw q("It is not a typed array constructor!");return new t(e)},kt=function(t,e){return Pt(I(t,t[yt]),e)},Pt=function(t,e){for(var r=0,n=e.length,i=At(t,n);n>r;)i[r]=e[r++];return i},Ct=function(t,e,r){H(t,e,{get:function(){return this._d[r]}})},Tt=function t(e){var r,n,i,o,s,a,u=w(e),f=arguments.length,h=f>1?arguments[1]:void 0,l=void 0!==h,p=k(u);if(void 0!=p&&!F(p)){for(a=p.call(u),i=[],r=0;!(s=a.next()).done;r++)i.push(s.value);u=i}for(l&&f>2&&(h=c(h,arguments[2],2)),r=0,n=g(u.length),o=At(this,n);n>r;r++)o[r]=l?h(u[r],r):u[r];return o},Rt=function t(){for(var e=0,r=arguments.length,n=At(this,r);r>e;)n[e]=arguments[e++];return n},It=!!W&&o(function(){pt.call(new W(1))}),Ot=function t(){return pt.apply(It?ht.call(xt(this)):xt(this),arguments)},Dt={copyWithin:function t(e,r){return j.call(xt(this),e,r,arguments.length>2?arguments[2]:void 0)},every:function t(e){return Q(xt(this),e,arguments.length>1?arguments[1]:void 0)},fill:function t(e){return M.apply(xt(this),arguments)},filter:function t(e){return kt(this,X(xt(this),e,arguments.length>1?arguments[1]:void 0))},find:function t(e){return Z(xt(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function t(e){return tt(xt(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function t(e){G(xt(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function t(e){return rt(xt(this),e,arguments.length>1?arguments[1]:void 0)},includes:function t(e){return et(xt(this),e,arguments.length>1?arguments[1]:void 0)},join:function t(e){return ct.apply(xt(this),arguments)},lastIndexOf:function t(e){return st.apply(xt(this),arguments)},map:function t(e){return bt(xt(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function t(e){return at.apply(xt(this),arguments)},reduceRight:function t(e){return ut.apply(xt(this),arguments)},reverse:function t(){for(var e,r=xt(this).length,n=Math.floor(r/2),i=0;i1?arguments[1]:void 0)},sort:function t(e){return ft.call(xt(this),e)},subarray:function t(e,r){var n=xt(this),i=n.length,o=y(e,i);return new(I(n,n[yt]))(n.buffer,n.byteOffset+o*n.BYTES_PER_ELEMENT,g((void 0===r?i:y(r,i))-o))}},Nt=function t(e,r){return kt(this,ht.call(xt(this),e,r))},Lt=function t(e){xt(this);var r=Et(arguments[1],1),n=this.length,i=w(e),o=g(i.length),s=0;if(o+r>n)throw K("Wrong length!");for(;s255?255:255&n),i.v[p](r*e+i.o,n,wt)}(this,r,t)},enumerable:!0})};_?(d=r(function(t,r,n,i){f(t,d,c,"_d");var o,s,a,u,h=0,p=0;if(b(r)){if(!(r instanceof z||"ArrayBuffer"==(u=S(r))||"SharedArrayBuffer"==u))return _t in r?Pt(d,r):Tt.call(d,r);o=r,p=Et(n,e);var y=r.byteLength;if(void 0===i){if(y%e)throw K("Wrong length!");if((s=y-p)<0)throw K("Wrong length!")}else if((s=g(i)*e)+p>y)throw K("Wrong length!");a=s/e}else a=v(r),o=new z(s=a*e);for(l(t,"_d",{b:o,o:p,l:s,e:a,v:new Y(o)});hdocument.F=Object<\/script>"),t.close(),u=t.F;n--;)delete u.prototype[o[n]];return u()};t.exports=Object.create||function t(e,r){var o;return null!==e?(a.prototype=n(e),o=new a,a.prototype=null,o[s]=e):o=u(),void 0===r?o:i(o,r)}},function(t,e,r){var n=r(105),i=r(75).concat("length","prototype");e.f=Object.getOwnPropertyNames||function t(e){return n(e,i)}},function(t,e,r){"use strict";var n=r(2),i=r(9),o=r(8),s=r(6)("species");t.exports=function(t){var e=n[t];o&&e&&!e[s]&&i.f(e,s,{configurable:!0,get:function(){return this}})}},function(t,e){t.exports=function(t,e,r,n){if(!(t instanceof e)||void 0!==n&&n in t)throw TypeError(r+": incorrect invocation!");return t}},function(t,e,r){var n=r(20),i=r(118),o=r(87),s=r(1),a=r(7),u=r(89),c={},f={};(e=t.exports=function(t,e,r,h,l){var p,d,g,v,y=l?function(){return t}:u(t),m=n(r,h,e?2:1),_=0;if("function"!=typeof y)throw TypeError(t+" is not iterable!");if(o(y)){for(p=a(t.length);p>_;_++)if((v=e?m(s(d=t[_])[0],d[1]):m(t[_]))===c||v===f)return v}else for(g=y.call(t);!(d=g.next()).done;)if((v=i(g,m,d.value,e))===c||v===f)return v}).BREAK=c,e.RETURN=f},function(t,e,r){var n=r(13);t.exports=function(t,e,r){for(var i in e)n(t,i,e[i],r);return t}},function(t,e,r){var n=r(5);t.exports=function(t,e){if(!n(t)||t._t!==e)throw TypeError("Incompatible receiver, "+e+" required!");return t}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:o.JsonService;if(function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw i.Log.error("MetadataService: No settings passed to MetadataService"),new Error("settings");this._settings=e,this._jsonService=new r(["application/jwk-set+json"])}return t.prototype.getMetadata=function t(){var e=this;return this._settings.metadata?(i.Log.debug("MetadataService.getMetadata: Returning metadata from settings"),Promise.resolve(this._settings.metadata)):this.metadataUrl?(i.Log.debug("MetadataService.getMetadata: getting metadata from",this.metadataUrl),this._jsonService.getJson(this.metadataUrl).then(function(t){return i.Log.debug("MetadataService.getMetadata: json received"),e._settings.metadata=t,t})):(i.Log.error("MetadataService.getMetadata: No authority or metadataUrl configured on settings"),Promise.reject(new Error("No authority or metadataUrl configured on settings")))},t.prototype.getIssuer=function t(){return this._getMetadataProperty("issuer")},t.prototype.getAuthorizationEndpoint=function t(){return this._getMetadataProperty("authorization_endpoint")},t.prototype.getUserInfoEndpoint=function t(){return this._getMetadataProperty("userinfo_endpoint")},t.prototype.getTokenEndpoint=function t(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];return this._getMetadataProperty("token_endpoint",e)},t.prototype.getCheckSessionIframe=function t(){return this._getMetadataProperty("check_session_iframe",!0)},t.prototype.getEndSessionEndpoint=function t(){return this._getMetadataProperty("end_session_endpoint",!0)},t.prototype.getRevocationEndpoint=function t(){return this._getMetadataProperty("revocation_endpoint",!0)},t.prototype.getKeysEndpoint=function t(){return this._getMetadataProperty("jwks_uri",!0)},t.prototype._getMetadataProperty=function t(e){var r=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return i.Log.debug("MetadataService.getMetadataProperty for: "+e),this.getMetadata().then(function(t){if(i.Log.debug("MetadataService.getMetadataProperty: metadata recieved"),void 0===t[e]){if(!0===r)return void i.Log.warn("MetadataService.getMetadataProperty: Metadata does not contain optional property "+e);throw i.Log.error("MetadataService.getMetadataProperty: Metadata does not contain property "+e),new Error("Metadata does not contain property "+e)}return t[e]})},t.prototype.getSigningKeys=function t(){var e=this;return this._settings.signingKeys?(i.Log.debug("MetadataService.getSigningKeys: Returning signingKeys from settings"),Promise.resolve(this._settings.signingKeys)):this._getMetadataProperty("jwks_uri").then(function(t){return i.Log.debug("MetadataService.getSigningKeys: jwks_uri received",t),e._jsonService.getJson(t).then(function(t){if(i.Log.debug("MetadataService.getSigningKeys: key set received",t),!t.keys)throw i.Log.error("MetadataService.getSigningKeys: Missing keys on keyset"),new Error("Missing keys on keyset");return e._settings.signingKeys=t.keys,e._settings.signingKeys})})},n(t,[{key:"metadataUrl",get:function t(){return this._metadataUrl||(this._settings.metadataUrl?this._metadataUrl=this._settings.metadataUrl:(this._metadataUrl=this._settings.authority,this._metadataUrl&&this._metadataUrl.indexOf(".well-known/openid-configuration")<0&&("/"!==this._metadataUrl[this._metadataUrl.length-1]&&(this._metadataUrl+="/"),this._metadataUrl+=".well-known/openid-configuration"))),this._metadataUrl}}]),t}()},function(t,e,r){var n=r(19),i=r(2),o=i["__core-js_shared__"]||(i["__core-js_shared__"]={});(t.exports=function(t,e){return o[t]||(o[t]=void 0!==e?e:{})})("versions",[]).push({version:n.version,mode:r(30)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(t,e,r){var n=r(21);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==n(t)?t.split(""):Object(t)}},function(t,e){e.f={}.propertyIsEnumerable},function(t,e,r){"use strict";var n=r(1);t.exports=function(){var t=n(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},function(t,e,r){var n=r(1),i=r(11),o=r(6)("species");t.exports=function(t,e){var r,s=n(t).constructor;return void 0===s||void 0==(r=n(s)[o])?e:i(r)}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UrlUtility=void 0;var n=r(3),i=r(44);e.UrlUtility=function(){function t(){!function e(t,r){if(!(t instanceof r))throw new TypeError("Cannot call a class as a function")}(this,t)}return t.addQueryParam=function t(e,r,n){return e.indexOf("?")<0&&(e+="?"),"?"!==e[e.length-1]&&(e+="&"),e+=encodeURIComponent(r),e+="=",e+=encodeURIComponent(n)},t.parseUrlFragment=function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"#",o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.Global;"string"!=typeof e&&(e=o.location.href);var s=e.lastIndexOf(r);s>=0&&(e=e.substr(s+1)),"?"===r&&(s=e.indexOf("#"))>=0&&(e=e.substr(0,s));for(var a,u={},c=/([^&=]+)=([^&]*)/g,f=0;a=c.exec(e);)if(u[decodeURIComponent(a[1])]=decodeURIComponent(a[2]),f++>50)return n.Log.error("UrlUtility.parseUrlFragment: response exceeded expected number of parameters",e),{error:"Response exceeded expected number of parameters"};for(var h in u)return u;return{}},t}()},function(t,e,r){var n=r(16),i=r(7),o=r(36);t.exports=function(t){return function(e,r,s){var a,u=n(e),c=i(u.length),f=o(s,c);if(t&&r!=r){for(;c>f;)if((a=u[f++])!=a)return!0}else for(;c>f;f++)if((t||f in u)&&u[f]===r)return t||f||0;return!t&&-1}}},function(t,e){e.f=Object.getOwnPropertySymbols},function(t,e,r){var n=r(21);t.exports=Array.isArray||function t(e){return"Array"==n(e)}},function(t,e,r){var n=r(22),i=r(25);t.exports=function(t){return function(e,r){var o,s,a=String(i(e)),u=n(r),c=a.length;return u<0||u>=c?t?"":void 0:(o=a.charCodeAt(u))<55296||o>56319||u+1===c||(s=a.charCodeAt(u+1))<56320||s>57343?t?a.charAt(u):o:t?a.slice(u,u+2):s-56320+(o-55296<<10)+65536}}},function(t,e,r){var n=r(5),i=r(21),o=r(6)("match");t.exports=function(t){var e;return n(t)&&(void 0!==(e=t[o])?!!e:"RegExp"==i(t))}},function(t,e,r){var n=r(6)("iterator"),i=!1;try{var o=[7][n]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(t){}t.exports=function(t,e){if(!e&&!i)return!1;var r=!1;try{var o=[7],s=o[n]();s.next=function(){return{done:r=!0}},o[n]=function(){return s},t(o)}catch(t){}return r}},function(t,e,r){"use strict";var n=r(46),i=RegExp.prototype.exec;t.exports=function(t,e){var r=t.exec;if("function"==typeof r){var o=r.call(t,e);if("object"!=typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==n(t))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(t,e)}},function(t,e,r){"use strict";r(122);var n=r(13),i=r(12),o=r(4),s=r(25),a=r(6),u=r(93),c=a("species"),f=!o(function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")}),h=function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var r="ab".split(t);return 2===r.length&&"a"===r[0]&&"b"===r[1]}();t.exports=function(t,e,r){var l=a(t),p=!o(function(){var e={};return e[l]=function(){return 7},7!=""[t](e)}),d=p?!o(function(){var e=!1,r=/a/;return r.exec=function(){return e=!0,null},"split"===t&&(r.constructor={},r.constructor[c]=function(){return r}),r[l](""),!e}):void 0;if(!p||!d||"replace"===t&&!f||"split"===t&&!h){var g=/./[l],v=r(s,l,""[t],function t(e,r,n,i,o){return r.exec===u?p&&!o?{done:!0,value:g.call(r,n,i)}:{done:!0,value:e.call(n,r,i)}:{done:!1}}),y=v[0],m=v[1];n(String.prototype,t,y),i(RegExp.prototype,l,2==e?function(t,e){return m.call(t,this,e)}:function(t){return m.call(t,this)})}}},function(t,e,r){var n=r(2).navigator;t.exports=n&&n.userAgent||""},function(t,e,r){"use strict";var n=r(2),i=r(0),o=r(13),s=r(42),a=r(31),u=r(41),c=r(40),f=r(5),h=r(4),l=r(61),p=r(45),d=r(79);t.exports=function(t,e,r,g,v,y){var m=n[t],_=m,S=v?"set":"add",b=_&&_.prototype,w={},F=function(t){var e=b[t];o(b,t,"delete"==t?function(t){return!(y&&!f(t))&&e.call(this,0===t?0:t)}:"has"==t?function t(r){return!(y&&!f(r))&&e.call(this,0===r?0:r)}:"get"==t?function t(r){return y&&!f(r)?void 0:e.call(this,0===r?0:r)}:"add"==t?function t(r){return e.call(this,0===r?0:r),this}:function t(r,n){return e.call(this,0===r?0:r,n),this})};if("function"==typeof _&&(y||b.forEach&&!h(function(){(new _).entries().next()}))){var E=new _,x=E[S](y?{}:-0,1)!=E,A=h(function(){E.has(1)}),k=l(function(t){new _(t)}),P=!y&&h(function(){for(var t=new _,e=5;e--;)t[S](e,e);return!t.has(-0)});k||((_=e(function(e,r){c(e,_,t);var n=d(new m,e,_);return void 0!=r&&u(r,v,n[S],n),n})).prototype=b,b.constructor=_),(A||P)&&(F("delete"),F("has"),v&&F("get")),(P||x)&&F(S),y&&b.clear&&delete b.clear}else _=g.getConstructor(e,t,v,S),s(_.prototype,r),a.NEED=!0;return p(_,t),w[t]=_,i(i.G+i.W+i.F*(_!=m),w),y||g.setStrong(_,t,v),_}},function(t,e,r){for(var n,i=r(2),o=r(12),s=r(34),a=s("typed_array"),u=s("view"),c=!(!i.ArrayBuffer||!i.DataView),f=c,h=0,l="Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array".split(",");h<9;)(n=i[l[h++]])?(o(n.prototype,a,!0),o(n.prototype,u,!0)):f=!1;t.exports={ABV:c,CONSTR:f,TYPED:a,VIEW:u}},function(t,e,r){"use strict";t.exports=r(30)||!r(4)(function(){var t=Math.random();__defineSetter__.call(null,t,function(){}),delete r(2)[t]})},function(t,e,r){"use strict";var n=r(0);t.exports=function(t){n(n.S,t,{of:function t(){for(var e=arguments.length,r=new Array(e);e--;)r[e]=arguments[e];return new this(r)}})}},function(t,e,r){"use strict";var n=r(0),i=r(11),o=r(20),s=r(41);t.exports=function(t){n(n.S,t,{from:function t(e){var r,n,a,u,c=arguments[1];return i(this),(r=void 0!==c)&&i(c),void 0==e?new this:(n=[],r?(a=0,u=o(c,arguments[2],2),s(e,!1,function(t){n.push(u(t,a++))})):s(e,!1,n.push,n),new this(n))}})}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.JoseUtil=void 0;var n=r(358),i=function o(t){return t&&t.__esModule?t:{default:t}}(r(364));e.JoseUtil=(0,i.default)({jws:n.jws,KeyUtil:n.KeyUtil,X509:n.X509,crypto:n.crypto,hextob64u:n.hextob64u,b64tohex:n.b64tohex,AllowedSigningAlgs:n.AllowedSigningAlgs})},function(t,e){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(t){"object"==typeof window&&(r=window)}t.exports=r},function(t,e,r){var n=r(5),i=r(2).document,o=n(i)&&n(i.createElement);t.exports=function(t){return o?i.createElement(t):{}}},function(t,e,r){var n=r(2),i=r(19),o=r(30),s=r(104),a=r(9).f;t.exports=function(t){var e=i.Symbol||(i.Symbol=o?{}:n.Symbol||{});"_"==t.charAt(0)||t in e||a(e,t,{value:s.f(t)})}},function(t,e,r){var n=r(50)("keys"),i=r(34);t.exports=function(t){return n[t]||(n[t]=i(t))}},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e,r){var n=r(2).document;t.exports=n&&n.documentElement},function(t,e,r){var n=r(5),i=r(1),o=function(t,e){if(i(t),!n(e)&&null!==e)throw TypeError(e+": can't set as prototype!")};t.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(t,e,n){try{(n=r(20)(Function.call,r(17).f(Object.prototype,"__proto__").set,2))(t,[]),e=!(t instanceof Array)}catch(t){e=!0}return function t(r,i){return o(r,i),e?r.__proto__=i:n(r,i),r}}({},!1):void 0),check:o}},function(t,e){t.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},function(t,e,r){var n=r(5),i=r(77).set;t.exports=function(t,e,r){var o,s=e.constructor;return s!==r&&"function"==typeof s&&(o=s.prototype)!==r.prototype&&n(o)&&i&&i(t,o),t}},function(t,e,r){"use strict";var n=r(22),i=r(25);t.exports=function t(e){var r=String(i(this)),o="",s=n(e);if(s<0||s==1/0)throw RangeError("Count can't be negative");for(;s>0;(s>>>=1)&&(r+=r))1&s&&(o+=r);return o}},function(t,e){t.exports=Math.sign||function t(e){return 0==(e=+e)||e!=e?e:e<0?-1:1}},function(t,e){var r=Math.expm1;t.exports=!r||r(10)>22025.465794806718||r(10)<22025.465794806718||-2e-17!=r(-2e-17)?function t(e){return 0==(e=+e)?e:e>-1e-6&&e<1e-6?e+e*e/2:Math.exp(e)-1}:r},function(t,e,r){"use strict";var n=r(30),i=r(0),o=r(13),s=r(12),a=r(48),u=r(84),c=r(45),f=r(18),h=r(6)("iterator"),l=!([].keys&&"next"in[].keys()),p=function(){return this};t.exports=function(t,e,r,d,g,v,y){u(r,e,d);var m,_,S,b=function(t){if(!l&&t in x)return x[t];switch(t){case"keys":return function e(){return new r(this,t)};case"values":return function e(){return new r(this,t)}}return function e(){return new r(this,t)}},w=e+" Iterator",F="values"==g,E=!1,x=t.prototype,A=x[h]||x["@@iterator"]||g&&x[g],k=A||b(g),P=g?F?b("entries"):k:void 0,C="Array"==e&&x.entries||A;if(C&&(S=f(C.call(new t)))!==Object.prototype&&S.next&&(c(S,w,!0),n||"function"==typeof S[h]||s(S,h,p)),F&&A&&"values"!==A.name&&(E=!0,k=function t(){return A.call(this)}),n&&!y||!l&&!E&&x[h]||s(x,h,k),a[e]=k,a[w]=p,g)if(m={values:F?k:b("values"),keys:v?k:b("keys"),entries:P},y)for(_ in m)_ in x||o(x,_,m[_]);else i(i.P+i.F*(l||E),e,m);return m}},function(t,e,r){"use strict";var n=r(37),i=r(33),o=r(45),s={};r(12)(s,r(6)("iterator"),function(){return this}),t.exports=function(t,e,r){t.prototype=n(s,{next:i(1,r)}),o(t,e+" Iterator")}},function(t,e,r){var n=r(60),i=r(25);t.exports=function(t,e,r){if(n(e))throw TypeError("String#"+r+" doesn't accept regex!");return String(i(t))}},function(t,e,r){var n=r(6)("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(r){try{return e[n]=!1,!"/./"[t](e)}catch(t){}}return!0}},function(t,e,r){var n=r(48),i=r(6)("iterator"),o=Array.prototype;t.exports=function(t){return void 0!==t&&(n.Array===t||o[i]===t)}},function(t,e,r){"use strict";var n=r(9),i=r(33);t.exports=function(t,e,r){e in t?n.f(t,e,i(0,r)):t[e]=r}},function(t,e,r){var n=r(46),i=r(6)("iterator"),o=r(48);t.exports=r(19).getIteratorMethod=function(t){if(void 0!=t)return t[i]||t["@@iterator"]||o[n(t)]}},function(t,e,r){var n=r(245);t.exports=function(t,e){return new(n(t))(e)}},function(t,e,r){"use strict";var n=r(10),i=r(36),o=r(7);t.exports=function t(e){for(var r=n(this),s=o(r.length),a=arguments.length,u=i(a>1?arguments[1]:void 0,s),c=a>2?arguments[2]:void 0,f=void 0===c?s:i(c,s);f>u;)r[u++]=e;return r}},function(t,e,r){"use strict";var n=r(32),i=r(121),o=r(48),s=r(16);t.exports=r(83)(Array,"Array",function(t,e){this._t=s(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,r=this._i++;return!t||r>=t.length?(this._t=void 0,i(1)):i(0,"keys"==e?r:"values"==e?t[r]:[r,t[r]])},"values"),o.Arguments=o.Array,n("keys"),n("values"),n("entries")},function(t,e,r){"use strict";var n,i,o=r(53),s=RegExp.prototype.exec,a=String.prototype.replace,u=s,c=(n=/a/,i=/b*/g,s.call(n,"a"),s.call(i,"a"),0!==n.lastIndex||0!==i.lastIndex),f=void 0!==/()??/.exec("")[1];(c||f)&&(u=function t(e){var r,n,i,u,h=this;return f&&(n=new RegExp("^"+h.source+"$(?!\\s)",o.call(h))),c&&(r=h.lastIndex),i=s.call(h,e),c&&i&&(h.lastIndex=h.global?i.index+i[0].length:r),f&&i&&i.length>1&&a.call(i[0],n,function(){for(u=1;ui;)r.push(arguments[i++]);return y[++v]=function(){a("function"==typeof e?e:Function(e),r)},n(v),v},p=function t(e){delete y[e]},"process"==r(21)(h)?n=function(t){h.nextTick(s(m,t,1))}:g&&g.now?n=function(t){g.now(s(m,t,1))}:d?(o=(i=new d).port2,i.port1.onmessage=_,n=s(o.postMessage,o,1)):f.addEventListener&&"function"==typeof postMessage&&!f.importScripts?(n=function(t){f.postMessage(t+"","*")},f.addEventListener("message",_,!1)):n="onreadystatechange"in c("script")?function(t){u.appendChild(c("script")).onreadystatechange=function(){u.removeChild(this),m.call(t)}}:function(t){setTimeout(s(m,t,1),0)}),t.exports={set:l,clear:p}},function(t,e,r){var n=r(2),i=r(95).set,o=n.MutationObserver||n.WebKitMutationObserver,s=n.process,a=n.Promise,u="process"==r(21)(s);t.exports=function(){var t,e,r,c=function(){var n,i;for(u&&(n=s.domain)&&n.exit();t;){i=t.fn,t=t.next;try{i()}catch(n){throw t?r():e=void 0,n}}e=void 0,n&&n.enter()};if(u)r=function(){s.nextTick(c)};else if(!o||n.navigator&&n.navigator.standalone)if(a&&a.resolve){var f=a.resolve(void 0);r=function(){f.then(c)}}else r=function(){i.call(n,c)};else{var h=!0,l=document.createTextNode("");new o(c).observe(l,{characterData:!0}),r=function(){l.data=h=!h}}return function(n){var i={fn:n,next:void 0};e&&(e.next=i),t||(t=i,r()),e=i}}},function(t,e,r){"use strict";var n=r(11);t.exports.f=function(t){return new function e(t){var e,r;this.promise=new t(function(t,n){if(void 0!==e||void 0!==r)throw TypeError("Bad Promise constructor");e=t,r=n}),this.resolve=n(e),this.reject=n(r)}(t)}},function(t,e,r){"use strict";var n=r(2),i=r(8),o=r(30),s=r(66),a=r(12),u=r(42),c=r(4),f=r(40),h=r(22),l=r(7),p=r(131),d=r(38).f,g=r(9).f,v=r(91),y=r(45),m="prototype",_="Wrong index!",S=n.ArrayBuffer,b=n.DataView,w=n.Math,F=n.RangeError,E=n.Infinity,x=S,A=w.abs,k=w.pow,P=w.floor,C=w.log,T=w.LN2,R=i?"_b":"buffer",I=i?"_l":"byteLength",O=i?"_o":"byteOffset";function D(t,e,r){var n,i,o,s=new Array(r),a=8*r-e-1,u=(1<>1,f=23===e?k(2,-24)-k(2,-77):0,h=0,l=t<0||0===t&&1/t<0?1:0;for((t=A(t))!=t||t===E?(i=t!=t?1:0,n=u):(n=P(C(t)/T),t*(o=k(2,-n))<1&&(n--,o*=2),(t+=n+c>=1?f/o:f*k(2,1-c))*o>=2&&(n++,o/=2),n+c>=u?(i=0,n=u):n+c>=1?(i=(t*o-1)*k(2,e),n+=c):(i=t*k(2,c-1)*k(2,e),n=0));e>=8;s[h++]=255&i,i/=256,e-=8);for(n=n<0;s[h++]=255&n,n/=256,a-=8);return s[--h]|=128*l,s}function N(t,e,r){var n,i=8*r-e-1,o=(1<>1,a=i-7,u=r-1,c=t[u--],f=127&c;for(c>>=7;a>0;f=256*f+t[u],u--,a-=8);for(n=f&(1<<-a)-1,f>>=-a,a+=e;a>0;n=256*n+t[u],u--,a-=8);if(0===f)f=1-s;else{if(f===o)return n?NaN:c?-E:E;n+=k(2,e),f-=s}return(c?-1:1)*n*k(2,f-e)}function L(t){return t[3]<<24|t[2]<<16|t[1]<<8|t[0]}function M(t){return[255&t]}function j(t){return[255&t,t>>8&255]}function U(t){return[255&t,t>>8&255,t>>16&255,t>>24&255]}function B(t){return D(t,52,8)}function H(t){return D(t,23,4)}function V(t,e,r){g(t[m],e,{get:function(){return this[r]}})}function K(t,e,r,n){var i=p(+r);if(i+e>t[I])throw F(_);var o=t[R]._b,s=i+t[O],a=o.slice(s,s+e);return n?a:a.reverse()}function q(t,e,r,n,i,o){var s=p(+r);if(s+e>t[I])throw F(_);for(var a=t[R]._b,u=s+t[O],c=n(+i),f=0;fY;)(W=z[Y++])in S||a(S,W,x[W]);o||(J.constructor=S)}var G=new b(new S(2)),X=b[m].setInt8;G.setInt8(0,2147483648),G.setInt8(1,2147483649),!G.getInt8(0)&&G.getInt8(1)||u(b[m],{setInt8:function t(e,r){X.call(this,e,r<<24>>24)},setUint8:function t(e,r){X.call(this,e,r<<24>>24)}},!0)}else S=function t(e){f(this,S,"ArrayBuffer");var r=p(e);this._b=v.call(new Array(r),0),this[I]=r},b=function t(e,r,n){f(this,b,"DataView"),f(e,S,"DataView");var i=e[I],o=h(r);if(o<0||o>i)throw F("Wrong offset!");if(o+(n=void 0===n?i-o:l(n))>i)throw F("Wrong length!");this[R]=e,this[O]=o,this[I]=n},i&&(V(S,"byteLength","_l"),V(b,"buffer","_b"),V(b,"byteLength","_l"),V(b,"byteOffset","_o")),u(b[m],{getInt8:function t(e){return K(this,1,e)[0]<<24>>24},getUint8:function t(e){return K(this,1,e)[0]},getInt16:function t(e){var r=K(this,2,e,arguments[1]);return(r[1]<<8|r[0])<<16>>16},getUint16:function t(e){var r=K(this,2,e,arguments[1]);return r[1]<<8|r[0]},getInt32:function t(e){return L(K(this,4,e,arguments[1]))},getUint32:function t(e){return L(K(this,4,e,arguments[1]))>>>0},getFloat32:function t(e){return N(K(this,4,e,arguments[1]),23,4)},getFloat64:function t(e){return N(K(this,8,e,arguments[1]),52,8)},setInt8:function t(e,r){q(this,1,e,M,r)},setUint8:function t(e,r){q(this,1,e,M,r)},setInt16:function t(e,r){q(this,2,e,j,r,arguments[2])},setUint16:function t(e,r){q(this,2,e,j,r,arguments[2])},setInt32:function t(e,r){q(this,4,e,U,r,arguments[2])},setUint32:function t(e,r){q(this,4,e,U,r,arguments[2])},setFloat32:function t(e,r){q(this,4,e,H,r,arguments[2])},setFloat64:function t(e,r){q(this,8,e,B,r,arguments[2])}});y(S,"ArrayBuffer"),y(b,"DataView"),a(b[m],s.VIEW,!0),e.ArrayBuffer=S,e.DataView=b},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OidcClientSettings=void 0;var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},r=e.authority,i=e.metadataUrl,o=e.metadata,p=e.signingKeys,d=e.client_id,g=e.client_secret,v=e.response_type,y=void 0===v?c:v,m=e.scope,_=void 0===m?f:m,S=e.redirect_uri,b=e.post_logout_redirect_uri,w=e.prompt,F=e.display,E=e.max_age,x=e.ui_locales,A=e.acr_values,k=e.resource,P=e.response_mode,C=e.filterProtocolClaims,T=void 0===C||C,R=e.loadUserInfo,I=void 0===R||R,O=e.staleStateAge,D=void 0===O?h:O,N=e.clockSkew,L=void 0===N?l:N,M=e.userInfoJwtIssuer,j=void 0===M?"OP":M,U=e.stateStore,B=void 0===U?new s.WebStorageStateStore:U,H=e.ResponseValidatorCtor,V=void 0===H?a.ResponseValidator:H,K=e.MetadataServiceCtor,q=void 0===K?u.MetadataService:K,W=e.extraQueryParams,J=void 0===W?{}:W;!function z(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._authority=r,this._metadataUrl=i,this._metadata=o,this._signingKeys=p,this._client_id=d,this._client_secret=g,this._response_type=y,this._scope=_,this._redirect_uri=S,this._post_logout_redirect_uri=b,this._prompt=w,this._display=F,this._max_age=E,this._ui_locales=x,this._acr_values=A,this._resource=k,this._response_mode=P,this._filterProtocolClaims=!!T,this._loadUserInfo=!!I,this._staleStateAge=D,this._clockSkew=L,this._userInfoJwtIssuer=j,this._stateStore=B,this._validator=new V(this),this._metadataService=new q(this),this._extraQueryParams="object"===(void 0===J?"undefined":n(J))?J:{}}return i(t,[{key:"client_id",get:function t(){return this._client_id},set:function t(e){if(this._client_id)throw o.Log.error("OidcClientSettings.set_client_id: client_id has already been assigned."),new Error("client_id has already been assigned.");this._client_id=e}},{key:"client_secret",get:function t(){return this._client_secret}},{key:"response_type",get:function t(){return this._response_type}},{key:"scope",get:function t(){return this._scope}},{key:"redirect_uri",get:function t(){return this._redirect_uri}},{key:"post_logout_redirect_uri",get:function t(){return this._post_logout_redirect_uri}},{key:"prompt",get:function t(){return this._prompt}},{key:"display",get:function t(){return this._display}},{key:"max_age",get:function t(){return this._max_age}},{key:"ui_locales",get:function t(){return this._ui_locales}},{key:"acr_values",get:function t(){return this._acr_values}},{key:"resource",get:function t(){return this._resource}},{key:"response_mode",get:function t(){return this._response_mode}},{key:"authority",get:function t(){return this._authority},set:function t(e){if(this._authority)throw o.Log.error("OidcClientSettings.set_authority: authority has already been assigned."),new Error("authority has already been assigned.");this._authority=e}},{key:"metadataUrl",get:function t(){return this._metadataUrl||(this._metadataUrl=this.authority,this._metadataUrl&&this._metadataUrl.indexOf(".well-known/openid-configuration")<0&&("/"!==this._metadataUrl[this._metadataUrl.length-1]&&(this._metadataUrl+="/"),this._metadataUrl+=".well-known/openid-configuration")),this._metadataUrl}},{key:"metadata",get:function t(){return this._metadata},set:function t(e){this._metadata=e}},{key:"signingKeys",get:function t(){return this._signingKeys},set:function t(e){this._signingKeys=e}},{key:"filterProtocolClaims",get:function t(){return this._filterProtocolClaims}},{key:"loadUserInfo",get:function t(){return this._loadUserInfo}},{key:"staleStateAge",get:function t(){return this._staleStateAge}},{key:"clockSkew",get:function t(){return this._clockSkew}},{key:"userInfoJwtIssuer",get:function t(){return this._userInfoJwtIssuer}},{key:"stateStore",get:function t(){return this._stateStore}},{key:"validator",get:function t(){return this._validator}},{key:"metadataService",get:function t(){return this._metadataService}},{key:"extraQueryParams",get:function t(){return this._extraQueryParams},set:function t(e){"object"===(void 0===e?"undefined":n(e))?this._extraQueryParams=e:this._extraQueryParams={}}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WebStorageStateStore=void 0;var n=r(3),i=r(44);e.WebStorageStateStore=function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.prefix,n=void 0===r?"oidc.":r,o=e.store,s=void 0===o?i.Global.localStorage:o;!function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._store=s,this._prefix=n}return t.prototype.set=function t(e,r){return n.Log.debug("WebStorageStateStore.set",e),e=this._prefix+e,this._store.setItem(e,r),Promise.resolve()},t.prototype.get=function t(e){n.Log.debug("WebStorageStateStore.get",e),e=this._prefix+e;var r=this._store.getItem(e);return Promise.resolve(r)},t.prototype.remove=function t(e){n.Log.debug("WebStorageStateStore.remove",e),e=this._prefix+e;var r=this._store.getItem(e);return this._store.removeItem(e),Promise.resolve(r)},t.prototype.getAllKeys=function t(){n.Log.debug("WebStorageStateStore.getAllKeys");for(var e=[],r=0;r0&&void 0!==arguments[0]?arguments[0]:null,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:i.Global.XMLHttpRequest,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;!function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),e&&Array.isArray(e)?this._contentTypes=e.slice():this._contentTypes=[],this._contentTypes.push("application/json"),n&&this._contentTypes.push("application/jwt"),this._XMLHttpRequest=r,this._jwtHandler=n}return t.prototype.getJson=function t(e,r){var i=this;if(!e)throw n.Log.error("JsonService.getJson: No url passed"),new Error("url");return n.Log.debug("JsonService.getJson, url: ",e),new Promise(function(t,o){var s=new i._XMLHttpRequest;s.open("GET",e);var a=i._contentTypes,u=i._jwtHandler;s.onload=function(){if(n.Log.debug("JsonService.getJson: HTTP response received, status",s.status),200===s.status){var r=s.getResponseHeader("Content-Type");if(r){var i=a.find(function(t){if(r.startsWith(t))return!0});if("application/jwt"==i)return void u(s).then(t,o);if(i)try{return void t(JSON.parse(s.responseText))}catch(t){return n.Log.error("JsonService.getJson: Error parsing JSON response",t.message),void o(t)}}o(Error("Invalid response Content-Type: "+r+", from URL: "+e))}else o(Error(s.statusText+" ("+s.status+")"))},s.onerror=function(){n.Log.error("JsonService.getJson: network error"),o(Error("Network Error"))},r&&(n.Log.debug("JsonService.getJson: token passed, setting Authorization header"),s.setRequestHeader("Authorization","Bearer "+r)),s.send()})},t.prototype.postForm=function t(e,r){var i=this;if(!e)throw n.Log.error("JsonService.postForm: No url passed"),new Error("url");return n.Log.debug("JsonService.postForm, url: ",e),new Promise(function(t,o){var s=new i._XMLHttpRequest;s.open("POST",e);var a=i._contentTypes;s.onload=function(){if(n.Log.debug("JsonService.postForm: HTTP response received, status",s.status),200!==s.status){if(400===s.status)if(i=s.getResponseHeader("Content-Type"))if(a.find(function(t){if(i.startsWith(t))return!0}))try{var r=JSON.parse(s.responseText);if(r&&r.error)return n.Log.error("JsonService.postForm: Error from server: ",r.error),void o(new Error(r.error))}catch(t){return n.Log.error("JsonService.postForm: Error parsing JSON response",t.message),void o(t)}o(Error(s.statusText+" ("+s.status+")"))}else{var i;if((i=s.getResponseHeader("Content-Type"))&&a.find(function(t){if(i.startsWith(t))return!0}))try{return void t(JSON.parse(s.responseText))}catch(t){return n.Log.error("JsonService.postForm: Error parsing JSON response",t.message),void o(t)}o(Error("Invalid response Content-Type: "+i+", from URL: "+e))}},s.onerror=function(){n.Log.error("JsonService.postForm: network error"),o(Error("Network Error"))};var u="";for(var c in r){var f=r[c];f&&(u.length>0&&(u+="&"),u+=encodeURIComponent(c),u+="=",u+=encodeURIComponent(f))}s.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),s.send(u)})},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.State=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},r=e.id,n=e.data,i=e.created,s=e.request_type;!function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._id=r||(0,o.default)(),this._data=n,this._created="number"==typeof i&&i>0?i:parseInt(Date.now()/1e3),this._request_type=s}return t.prototype.toStorageString=function t(){return i.Log.debug("State.toStorageString"),JSON.stringify({id:this.id,data:this.data,created:this.created,request_type:this.request_type})},t.fromStorageString=function e(r){return i.Log.debug("State.fromStorageString"),new t(JSON.parse(r))},t.clearStaleState=function e(r,n){var o=Date.now()/1e3-n;return r.getAllKeys().then(function(e){i.Log.debug("State.clearStaleState: got keys",e);for(var n=[],s=function s(a){var c=e[a];u=r.get(c).then(function(e){var n=!1;if(e)try{var s=t.fromStorageString(e);i.Log.debug("State.clearStaleState: got item from key: ",c,s.created),s.created<=o&&(n=!0)}catch(t){i.Log.error("State.clearStaleState: Error parsing state for key",c,t.message),n=!0}else i.Log.debug("State.clearStaleState: no item in storage for key: ",c),n=!0;if(n)return i.Log.debug("State.clearStaleState: removed item for key: ",c),r.remove(c)}),n.push(u)},a=0;au;)n(a,r=e[u++])&&(~o(c,r)||c.push(r));return c}},function(t,e,r){var n=r(9),i=r(1),o=r(35);t.exports=r(8)?Object.defineProperties:function t(e,r){i(e);for(var s,a=o(r),u=a.length,c=0;u>c;)n.f(e,s=a[c++],r[s]);return e}},function(t,e,r){var n=r(16),i=r(38).f,o={}.toString,s="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];t.exports.f=function t(e){return s&&"[object Window]"==o.call(e)?function(t){try{return i(t)}catch(t){return s.slice()}}(e):i(n(e))}},function(t,e,r){"use strict";var n=r(35),i=r(57),o=r(52),s=r(10),a=r(51),u=Object.assign;t.exports=!u||r(4)(function(){var t={},e={},r=Symbol(),n="abcdefghijklmnopqrst";return t[r]=7,n.split("").forEach(function(t){e[t]=t}),7!=u({},t)[r]||Object.keys(u({},e)).join("")!=n})?function t(e,r){for(var u=s(e),c=arguments.length,f=1,h=i.f,l=o.f;c>f;)for(var p,d=a(arguments[f++]),g=h?n(d).concat(h(d)):n(d),v=g.length,y=0;v>y;)l.call(d,p=g[y++])&&(u[p]=d[p]);return u}:u},function(t,e){t.exports=Object.is||function t(e,r){return e===r?0!==e||1/e==1/r:e!=e&&r!=r}},function(t,e,r){"use strict";var n=r(11),i=r(5),o=r(111),s=[].slice,a={};t.exports=Function.bind||function t(e){var r=n(this),u=s.call(arguments,1),c=function(){var t=u.concat(s.call(arguments));return this instanceof c?function(t,e,r){if(!(e in a)){for(var n=[],i=0;i>>0||(s.test(o)?16:10))}:n},function(t,e,r){var n=r(2).parseFloat,i=r(47).trim;t.exports=1/n(r(78)+"-0")!=-1/0?function t(e){var r=i(String(e),3),o=n(r);return 0===o&&"-"==r.charAt(0)?-0:o}:n},function(t,e,r){var n=r(21);t.exports=function(t,e){if("number"!=typeof t&&"Number"!=n(t))throw TypeError(e);return+t}},function(t,e,r){var n=r(5),i=Math.floor;t.exports=function t(e){return!n(e)&&isFinite(e)&&i(e)===e}},function(t,e){t.exports=Math.log1p||function t(e){return(e=+e)>-1e-8&&e<1e-8?e-e*e/2:Math.log(1+e)}},function(t,e,r){var n=r(81),i=Math.pow,o=i(2,-52),s=i(2,-23),a=i(2,127)*(2-s),u=i(2,-126);t.exports=Math.fround||function t(e){var r,i,c=Math.abs(e),f=n(e);return ca||i!=i?f*(1/0):f*i}},function(t,e,r){var n=r(1);t.exports=function(t,e,r,i){try{return i?e(n(r)[0],r[1]):e(r)}catch(e){var o=t.return;throw void 0!==o&&n(o.call(t)),e}}},function(t,e,r){var n=r(11),i=r(10),o=r(51),s=r(7);t.exports=function(t,e,r,a,u){n(e);var c=i(t),f=o(c),h=s(c.length),l=u?h-1:0,p=u?-1:1;if(r<2)for(;;){if(l in f){a=f[l],l+=p;break}if(l+=p,u?l<0:h<=l)throw TypeError("Reduce of empty array with no initial value")}for(;u?l>=0:h>l;l+=p)l in f&&(a=e(a,f[l],l,c));return a}},function(t,e,r){"use strict";var n=r(10),i=r(36),o=r(7);t.exports=[].copyWithin||function t(e,r){var s=n(this),a=o(s.length),u=i(e,a),c=i(r,a),f=arguments.length>2?arguments[2]:void 0,h=Math.min((void 0===f?a:i(f,a))-c,a-u),l=1;for(c0;)c in s?s[u]=s[c]:delete s[u],u+=l,c+=l;return s}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e,r){"use strict";var n=r(93);r(0)({target:"RegExp",proto:!0,forced:n!==/./.exec},{exec:n})},function(t,e,r){r(8)&&"g"!=/./g.flags&&r(9).f(RegExp.prototype,"flags",{configurable:!0,get:r(53)})},function(t,e){t.exports=function(t){try{return{e:!1,v:t()}}catch(t){return{e:!0,v:t}}}},function(t,e,r){var n=r(1),i=r(5),o=r(97);t.exports=function(t,e){if(n(t),i(e)&&e.constructor===t)return e;var r=o.f(t);return(0,r.resolve)(e),r.promise}},function(t,e,r){"use strict";var n=r(127),i=r(43);t.exports=r(65)("Map",function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},{get:function t(e){var r=n.getEntry(i(this,"Map"),e);return r&&r.v},set:function t(e,r){return n.def(i(this,"Map"),0===e?0:e,r)}},n,!0)},function(t,e,r){"use strict";var n=r(9).f,i=r(37),o=r(42),s=r(20),a=r(40),u=r(41),c=r(83),f=r(121),h=r(39),l=r(8),p=r(31).fastKey,d=r(43),g=l?"_s":"size",v=function(t,e){var r,n=p(e);if("F"!==n)return t._i[n];for(r=t._f;r;r=r.n)if(r.k==e)return r};t.exports={getConstructor:function(t,e,r,c){var f=t(function(t,n){a(t,f,e,"_i"),t._t=e,t._i=i(null),t._f=void 0,t._l=void 0,t[g]=0,void 0!=n&&u(n,r,t[c],t)});return o(f.prototype,{clear:function t(){for(var r=d(this,e),n=r._i,i=r._f;i;i=i.n)i.r=!0,i.p&&(i.p=i.p.n=void 0),delete n[i.i];r._f=r._l=void 0,r[g]=0},delete:function(t){var r=d(this,e),n=v(r,t);if(n){var i=n.n,o=n.p;delete r._i[n.i],n.r=!0,o&&(o.n=i),i&&(i.p=o),r._f==n&&(r._f=i),r._l==n&&(r._l=o),r[g]--}return!!n},forEach:function t(r){d(this,e);for(var n,i=s(r,arguments.length>1?arguments[1]:void 0,3);n=n?n.n:this._f;)for(i(n.v,n.k,this);n&&n.r;)n=n.p},has:function t(r){return!!v(d(this,e),r)}}),l&&n(f.prototype,"size",{get:function(){return d(this,e)[g]}}),f},def:function(t,e,r){var n,i,o=v(t,e);return o?o.v=r:(t._l=o={i:i=p(e,!0),k:e,v:r,p:n=t._l,n:void 0,r:!1},t._f||(t._f=o),n&&(n.n=o),t[g]++,"F"!==i&&(t._i[i]=o)),t},getEntry:v,setStrong:function(t,e,r){c(t,e,function(t,r){this._t=d(t,e),this._k=r,this._l=void 0},function(){for(var t=this._k,e=this._l;e&&e.r;)e=e.p;return this._t&&(this._l=e=e?e.n:this._t._f)?f(0,"keys"==t?e.k:"values"==t?e.v:[e.k,e.v]):(this._t=void 0,f(1))},r?"entries":"values",!r,!0),h(e)}}},function(t,e,r){"use strict";var n=r(127),i=r(43);t.exports=r(65)("Set",function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},{add:function t(e){return n.def(i(this,"Set"),e=0===e?0:e,e)}},n)},function(t,e,r){"use strict";var n,i=r(2),o=r(27)(0),s=r(13),a=r(31),u=r(108),c=r(130),f=r(5),h=r(43),l=r(43),p=!i.ActiveXObject&&"ActiveXObject"in i,d=a.getWeak,g=Object.isExtensible,v=c.ufstore,y=function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},m={get:function t(e){if(f(e)){var r=d(e);return!0===r?v(h(this,"WeakMap")).get(e):r?r[this._i]:void 0}},set:function t(e,r){return c.def(h(this,"WeakMap"),e,r)}},_=t.exports=r(65)("WeakMap",y,m,c,!0,!0);l&&p&&(u((n=c.getConstructor(y,"WeakMap")).prototype,m),a.NEED=!0,o(["delete","has","get","set"],function(t){var e=_.prototype,r=e[t];s(e,t,function(e,i){if(f(e)&&!g(e)){this._f||(this._f=new n);var o=this._f[t](e,i);return"set"==t?this:o}return r.call(this,e,i)})}))},function(t,e,r){"use strict";var n=r(42),i=r(31).getWeak,o=r(1),s=r(5),a=r(40),u=r(41),c=r(27),f=r(15),h=r(43),l=c(5),p=c(6),d=0,g=function(t){return t._l||(t._l=new v)},v=function(){this.a=[]},y=function(t,e){return l(t.a,function(t){return t[0]===e})};v.prototype={get:function(t){var e=y(this,t);if(e)return e[1]},has:function(t){return!!y(this,t)},set:function(t,e){var r=y(this,t);r?r[1]=e:this.a.push([t,e])},delete:function(t){var e=p(this.a,function(e){return e[0]===t});return~e&&this.a.splice(e,1),!!~e}},t.exports={getConstructor:function(t,e,r,o){var c=t(function(t,n){a(t,c,e,"_i"),t._t=e,t._i=d++,t._l=void 0,void 0!=n&&u(n,r,t[o],t)});return n(c.prototype,{delete:function(t){if(!s(t))return!1;var r=i(t);return!0===r?g(h(this,e)).delete(t):r&&f(r,this._i)&&delete r[this._i]},has:function t(r){if(!s(r))return!1;var n=i(r);return!0===n?g(h(this,e)).has(r):n&&f(n,this._i)}}),c},def:function(t,e,r){var n=i(o(e),!0);return!0===n?g(t).set(e,r):n[t._i]=r,t},ufstore:g}},function(t,e,r){var n=r(22),i=r(7);t.exports=function(t){if(void 0===t)return 0;var e=n(t),r=i(e);if(e!==r)throw RangeError("Wrong length!");return r}},function(t,e,r){var n=r(38),i=r(57),o=r(1),s=r(2).Reflect;t.exports=s&&s.ownKeys||function t(e){var r=n.f(o(e)),s=i.f;return s?r.concat(s(e)):r}},function(t,e,r){"use strict";var n=r(58),i=r(5),o=r(7),s=r(20),a=r(6)("isConcatSpreadable");t.exports=function t(e,r,u,c,f,h,l,p){for(var d,g,v=f,y=0,m=!!l&&s(l,p,3);y0)v=t(e,r,d,o(d.length),v,h-1)-1;else{if(v>=9007199254740991)throw TypeError();e[v]=d}v++}y++}return v}},function(t,e,r){var n=r(7),i=r(80),o=r(25);t.exports=function(t,e,r,s){var a=String(o(t)),u=a.length,c=void 0===r?" ":String(r),f=n(e);if(f<=u||""==c)return a;var h=f-u,l=i.call(c,Math.ceil(h/c.length));return l.length>h&&(l=l.slice(0,h)),s?l+a:a+l}},function(t,e,r){var n=r(35),i=r(16),o=r(52).f;t.exports=function(t){return function(e){for(var r,s=i(e),a=n(s),u=a.length,c=0,f=[];u>c;)o.call(s,r=a[c++])&&f.push(t?[r,s[r]]:s[r]);return f}}},function(t,e,r){var n=r(46),i=r(137);t.exports=function(t){return function e(){if(n(this)!=t)throw TypeError(t+"#toJSON isn't generic");return i(this)}}},function(t,e,r){var n=r(41);t.exports=function(t,e){var r=[];return n(t,!1,r.push,r,e),r}},function(t,e){t.exports=Math.scale||function t(e,r,n,i,o){return 0===arguments.length||e!=e||r!=r||n!=n||i!=i||o!=o?NaN:e===1/0||e===-1/0?e:(e-r)*(o-i)/(n-r)+i}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OidcClient=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{};!function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),e instanceof o.OidcClientSettings?this._settings=e:this._settings=new o.OidcClientSettings(e)}return t.prototype.createSigninRequest=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=r.response_type,o=r.scope,s=r.redirect_uri,u=r.data,c=r.state,f=r.prompt,h=r.display,l=r.max_age,p=r.ui_locales,d=r.id_token_hint,g=r.login_hint,v=r.acr_values,y=r.resource,m=r.request,_=r.request_uri,S=r.response_mode,b=r.extraQueryParams,w=r.extraTokenParams,F=r.request_type,E=r.skipUserInfo,x=arguments[1];i.Log.debug("OidcClient.createSigninRequest");var A=this._settings.client_id;n=n||this._settings.response_type,o=o||this._settings.scope,s=s||this._settings.redirect_uri,f=f||this._settings.prompt,h=h||this._settings.display,l=l||this._settings.max_age,p=p||this._settings.ui_locales,v=v||this._settings.acr_values,y=y||this._settings.resource,S=S||this._settings.response_mode,b=b||this._settings.extraQueryParams;var k=this._settings.authority;return a.SigninRequest.isCode(n)&&"code"!==n?Promise.reject(new Error("OpenID Connect hybrid flow is not supported")):this._metadataService.getAuthorizationEndpoint().then(function(t){i.Log.debug("OidcClient.createSigninRequest: Received authorization endpoint",t);var r=new a.SigninRequest({url:t,client_id:A,redirect_uri:s,response_type:n,scope:o,data:u||c,authority:k,prompt:f,display:h,max_age:l,ui_locales:p,id_token_hint:d,login_hint:g,acr_values:v,resource:y,request:m,request_uri:_,extraQueryParams:b,extraTokenParams:w,request_type:F,response_mode:S,client_secret:e._settings.client_secret,skipUserInfo:E}),P=r.state;return(x=x||e._stateStore).set(P.id,P.toStorageString()).then(function(){return r})})},t.prototype.readSigninResponseState=function t(e,r){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];i.Log.debug("OidcClient.readSigninResponseState");var o="query"===this._settings.response_mode||!this._settings.response_mode&&a.SigninRequest.isCode(this._settings.response_type)?"?":"#",s=new u.SigninResponse(e,o);return s.state?(r=r||this._stateStore,(n?r.remove.bind(r):r.get.bind(r))(s.state).then(function(t){if(!t)throw i.Log.error("OidcClient.readSigninResponseState: No matching state found in storage"),new Error("No matching state found in storage");return{state:h.SigninState.fromStorageString(t),response:s}})):(i.Log.error("OidcClient.readSigninResponseState: No state in response"),Promise.reject(new Error("No state in response")))},t.prototype.processSigninResponse=function t(e,r){var n=this;return i.Log.debug("OidcClient.processSigninResponse"),this.readSigninResponseState(e,r,!0).then(function(t){var e=t.state,r=t.response;return i.Log.debug("OidcClient.processSigninResponse: Received state from storage; validating response"),n._validator.validateSigninResponse(e,r)})},t.prototype.createSignoutRequest=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=r.id_token_hint,o=r.data,s=r.state,a=r.post_logout_redirect_uri,u=r.extraQueryParams,f=r.request_type,h=arguments[1];return i.Log.debug("OidcClient.createSignoutRequest"),a=a||this._settings.post_logout_redirect_uri,u=u||this._settings.extraQueryParams,this._metadataService.getEndSessionEndpoint().then(function(t){if(!t)throw i.Log.error("OidcClient.createSignoutRequest: No end session endpoint url returned"),new Error("no end session endpoint");i.Log.debug("OidcClient.createSignoutRequest: Received end session endpoint",t);var r=new c.SignoutRequest({url:t,id_token_hint:n,post_logout_redirect_uri:a,data:o||s,extraQueryParams:u,request_type:f}),l=r.state;return l&&(i.Log.debug("OidcClient.createSignoutRequest: Signout request has state to persist"),(h=h||e._stateStore).set(l.id,l.toStorageString())),r})},t.prototype.readSignoutResponseState=function t(e,r){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];i.Log.debug("OidcClient.readSignoutResponseState");var o=new f.SignoutResponse(e);if(!o.state)return i.Log.debug("OidcClient.readSignoutResponseState: No state in response"),o.error?(i.Log.warn("OidcClient.readSignoutResponseState: Response was error: ",o.error),Promise.reject(new s.ErrorResponse(o))):Promise.resolve({undefined:void 0,response:o});var a=o.state;return r=r||this._stateStore,(n?r.remove.bind(r):r.get.bind(r))(a).then(function(t){if(!t)throw i.Log.error("OidcClient.readSignoutResponseState: No matching state found in storage"),new Error("No matching state found in storage");return{state:l.State.fromStorageString(t),response:o}})},t.prototype.processSignoutResponse=function t(e,r){var n=this;return i.Log.debug("OidcClient.processSignoutResponse"),this.readSignoutResponseState(e,r,!0).then(function(t){var e=t.state,r=t.response;return e?(i.Log.debug("OidcClient.processSignoutResponse: Received state from storage; validating response"),n._validator.validateSignoutResponse(e,r)):(i.Log.debug("OidcClient.processSignoutResponse: No state from storage; skipping validating response"),r)})},t.prototype.clearStaleState=function t(e){return i.Log.debug("OidcClient.clearStaleState"),e=e||this._stateStore,l.State.clearStaleState(e,this.settings.staleStateAge)},n(t,[{key:"_stateStore",get:function t(){return this.settings.stateStore}},{key:"_validator",get:function t(){return this.settings.validator}},{key:"_metadataService",get:function t(){return this.settings.metadataService}},{key:"settings",get:function t(){return this._settings}},{key:"metadataService",get:function t(){return this._metadataService}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenClient=void 0;var n=r(101),i=r(49),o=r(3);e.TokenClient=function(){function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n.JsonService,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.MetadataService;if(function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw o.Log.error("TokenClient.ctor: No settings passed"),new Error("settings");this._settings=e,this._jsonService=new r,this._metadataService=new s(this._settings)}return t.prototype.exchangeCode=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(r=Object.assign({},r)).grant_type=r.grant_type||"authorization_code",r.client_id=r.client_id||this._settings.client_id,r.redirect_uri=r.redirect_uri||this._settings.redirect_uri,r.code?r.redirect_uri?r.code_verifier?r.client_id?this._metadataService.getTokenEndpoint(!1).then(function(t){return o.Log.debug("TokenClient.exchangeCode: Received token endpoint"),e._jsonService.postForm(t,r).then(function(t){return o.Log.debug("TokenClient.exchangeCode: response received"),t})}):(o.Log.error("TokenClient.exchangeCode: No client_id passed"),Promise.reject(new Error("A client_id is required"))):(o.Log.error("TokenClient.exchangeCode: No code_verifier passed"),Promise.reject(new Error("A code_verifier is required"))):(o.Log.error("TokenClient.exchangeCode: No redirect_uri passed"),Promise.reject(new Error("A redirect_uri is required"))):(o.Log.error("TokenClient.exchangeCode: No code passed"),Promise.reject(new Error("A code is required")))},t.prototype.exchangeRefreshToken=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(r=Object.assign({},r)).grant_type=r.grant_type||"refresh_token",r.client_id=r.client_id||this._settings.client_id,r.client_secret=r.client_secret||this._settings.client_secret,r.refresh_token?r.client_id?this._metadataService.getTokenEndpoint(!1).then(function(t){return o.Log.debug("TokenClient.exchangeRefreshToken: Received token endpoint"),e._jsonService.postForm(t,r).then(function(t){return o.Log.debug("TokenClient.exchangeRefreshToken: response received"),t})}):(o.Log.error("TokenClient.exchangeRefreshToken: No client_id passed"),Promise.reject(new Error("A client_id is required"))):(o.Log.error("TokenClient.exchangeRefreshToken: No refresh_token passed"),Promise.reject(new Error("A refresh_token is required")))},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ErrorResponse=void 0;var n=r(3);e.ErrorResponse=function(t){function e(){var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=r.error,o=r.error_description,s=r.error_uri,a=r.state;if(function u(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),!i)throw n.Log.error("No error passed to ErrorResponse"),new Error("error");var c=function f(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,o||i));return c.name="ErrorResponse",c.error=i,c.error_description=o,c.error_uri=s,c.state=a,c}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e}(Error)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SigninRequest=void 0;var n=r(3),i=r(55),o=r(143);e.SigninRequest=function(){function t(e){var r=e.url,s=e.client_id,a=e.redirect_uri,u=e.response_type,c=e.scope,f=e.authority,h=e.data,l=e.prompt,p=e.display,d=e.max_age,g=e.ui_locales,v=e.id_token_hint,y=e.login_hint,m=e.acr_values,_=e.resource,S=e.response_mode,b=e.request,w=e.request_uri,F=e.extraQueryParams,E=e.request_type,x=e.client_secret,A=e.extraTokenParams,k=e.skipUserInfo;if(function P(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!r)throw n.Log.error("SigninRequest.ctor: No url passed"),new Error("url");if(!s)throw n.Log.error("SigninRequest.ctor: No client_id passed"),new Error("client_id");if(!a)throw n.Log.error("SigninRequest.ctor: No redirect_uri passed"),new Error("redirect_uri");if(!u)throw n.Log.error("SigninRequest.ctor: No response_type passed"),new Error("response_type");if(!c)throw n.Log.error("SigninRequest.ctor: No scope passed"),new Error("scope");if(!f)throw n.Log.error("SigninRequest.ctor: No authority passed"),new Error("authority");var C=t.isOidc(u),T=t.isCode(u);S||(S=t.isCode(u)?"query":null),this.state=new o.SigninState({nonce:C,data:h,client_id:s,authority:f,redirect_uri:a,code_verifier:T,request_type:E,response_mode:S,client_secret:x,scope:c,extraTokenParams:A,skipUserInfo:k}),r=i.UrlUtility.addQueryParam(r,"client_id",s),r=i.UrlUtility.addQueryParam(r,"redirect_uri",a),r=i.UrlUtility.addQueryParam(r,"response_type",u),r=i.UrlUtility.addQueryParam(r,"scope",c),r=i.UrlUtility.addQueryParam(r,"state",this.state.id),C&&(r=i.UrlUtility.addQueryParam(r,"nonce",this.state.nonce)),T&&(r=i.UrlUtility.addQueryParam(r,"code_challenge",this.state.code_challenge),r=i.UrlUtility.addQueryParam(r,"code_challenge_method","S256"));var R={prompt:l,display:p,max_age:d,ui_locales:g,id_token_hint:v,login_hint:y,acr_values:m,resource:_,request:b,request_uri:w,response_mode:S};for(var I in R)R[I]&&(r=i.UrlUtility.addQueryParam(r,I,R[I]));for(var O in F)r=i.UrlUtility.addQueryParam(r,O,F[O]);this.url=r}return t.isOidc=function t(e){return!!e.split(/\s+/g).filter(function(t){return"id_token"===t})[0]},t.isOAuth=function t(e){return!!e.split(/\s+/g).filter(function(t){return"token"===t})[0]},t.isCode=function t(e){return!!e.split(/\s+/g).filter(function(t){return"code"===t})[0]},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SigninState=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},n=r.nonce,i=r.authority,o=r.client_id,u=r.redirect_uri,c=r.code_verifier,f=r.response_mode,h=r.client_secret,l=r.scope,p=r.extraTokenParams,d=r.skipUserInfo;!function g(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e);var v=function y(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,arguments[0]));if(!0===n?v._nonce=(0,a.default)():n&&(v._nonce=n),!0===c?v._code_verifier=(0,a.default)()+(0,a.default)()+(0,a.default)():c&&(v._code_verifier=c),v.code_verifier){var m=s.JoseUtil.hashString(v.code_verifier,"SHA256");v._code_challenge=s.JoseUtil.hexToBase64Url(m)}return v._redirect_uri=u,v._authority=i,v._client_id=o,v._response_mode=f,v._client_secret=h,v._scope=l,v._extraTokenParams=p,v._skipUserInfo=d,v}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype.toStorageString=function t(){return i.Log.debug("SigninState.toStorageString"),JSON.stringify({id:this.id,data:this.data,created:this.created,request_type:this.request_type,nonce:this.nonce,code_verifier:this.code_verifier,redirect_uri:this.redirect_uri,authority:this.authority,client_id:this.client_id,response_mode:this.response_mode,client_secret:this.client_secret,scope:this.scope,extraTokenParams:this.extraTokenParams,skipUserInfo:this.skipUserInfo})},e.fromStorageString=function t(r){return i.Log.debug("SigninState.fromStorageString"),new e(JSON.parse(r))},n(e,[{key:"nonce",get:function t(){return this._nonce}},{key:"authority",get:function t(){return this._authority}},{key:"client_id",get:function t(){return this._client_id}},{key:"redirect_uri",get:function t(){return this._redirect_uri}},{key:"code_verifier",get:function t(){return this._code_verifier}},{key:"code_challenge",get:function t(){return this._code_challenge}},{key:"response_mode",get:function t(){return this._response_mode}},{key:"client_secret",get:function t(){return this._client_secret}},{key:"scope",get:function t(){return this._scope}},{key:"extraTokenParams",get:function t(){return this._extraTokenParams}},{key:"skipUserInfo",get:function t(){return this._skipUserInfo}}]),e}(o.State)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default=function n(){return(0,i.default)().replace(/-/g,"")};var i=function o(t){return t&&t.__esModule?t:{default:t}}(r(365));t.exports=e.default},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.User=void 0;var n=function(){function t(t,e){for(var r=0;r0){var n=parseInt(Date.now()/1e3);this.expires_at=n+r}}},{key:"expired",get:function t(){var e=this.expires_in;if(void 0!==e)return e<=0}},{key:"scopes",get:function t(){return(this.scope||"").split(" ")}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AccessTokenEvents=void 0;var n=r(3),i=r(380);var o=60;e.AccessTokenEvents=function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.accessTokenExpiringNotificationTime,n=void 0===r?o:r,s=e.accessTokenExpiringTimer,a=void 0===s?new i.Timer("Access token expiring"):s,u=e.accessTokenExpiredTimer,c=void 0===u?new i.Timer("Access token expired"):u;!function f(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._accessTokenExpiringNotificationTime=n,this._accessTokenExpiring=a,this._accessTokenExpired=c}return t.prototype.load=function t(e){if(e.access_token&&void 0!==e.expires_in){var r=e.expires_in;if(n.Log.debug("AccessTokenEvents.load: access token present, remaining duration:",r),r>0){var i=r-this._accessTokenExpiringNotificationTime;i<=0&&(i=1),n.Log.debug("AccessTokenEvents.load: registering expiring timer in:",i),this._accessTokenExpiring.init(i)}else n.Log.debug("AccessTokenEvents.load: canceling existing expiring timer becase we're past expiration."),this._accessTokenExpiring.cancel();var o=r+1;n.Log.debug("AccessTokenEvents.load: registering expired timer in:",o),this._accessTokenExpired.init(o)}else this._accessTokenExpiring.cancel(),this._accessTokenExpired.cancel()},t.prototype.unload=function t(){n.Log.debug("AccessTokenEvents.unload: canceling existing access token timers"),this._accessTokenExpiring.cancel(),this._accessTokenExpired.cancel()},t.prototype.addAccessTokenExpiring=function t(e){this._accessTokenExpiring.addHandler(e)},t.prototype.removeAccessTokenExpiring=function t(e){this._accessTokenExpiring.removeHandler(e)},t.prototype.addAccessTokenExpired=function t(e){this._accessTokenExpired.addHandler(e)},t.prototype.removeAccessTokenExpired=function t(e){this._accessTokenExpired.removeHandler(e)},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Event=void 0;var n=r(3);e.Event=function(){function t(e){!function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._name=e,this._callbacks=[]}return t.prototype.addHandler=function t(e){this._callbacks.push(e)},t.prototype.removeHandler=function t(e){var r=this._callbacks.findIndex(function(t){return t===e});r>=0&&this._callbacks.splice(r,1)},t.prototype.raise=function t(){n.Log.debug("Event: Raising event: "+this._name);for(var e=0;e1&&void 0!==arguments[1]?arguments[1]:o.CheckSessionIFrame;if(function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw i.Log.error("SessionMonitor.ctor: No user manager passed to SessionMonitor"),new Error("userManager");this._userManager=e,this._CheckSessionIFrameCtor=n,this._userManager.events.addUserLoaded(this._start.bind(this)),this._userManager.events.addUserUnloaded(this._stop.bind(this)),this._userManager.getUser().then(function(t){t&&r._start(t)}).catch(function(t){i.Log.error("SessionMonitor ctor: error from getUser:",t.message)})}return t.prototype._start=function t(e){var r=this,n=e.session_state;n&&(this._sub=e.profile.sub,this._sid=e.profile.sid,i.Log.debug("SessionMonitor._start: session_state:",n,", sub:",this._sub),this._checkSessionIFrame?this._checkSessionIFrame.start(n):this._metadataService.getCheckSessionIframe().then(function(t){if(t){i.Log.debug("SessionMonitor._start: Initializing check session iframe");var e=r._client_id,o=r._checkSessionInterval,s=r._stopCheckSessionOnError;r._checkSessionIFrame=new r._CheckSessionIFrameCtor(r._callback.bind(r),e,t,o,s),r._checkSessionIFrame.load().then(function(){r._checkSessionIFrame.start(n)})}else i.Log.warn("SessionMonitor._start: No check session iframe found in the metadata")}).catch(function(t){i.Log.error("SessionMonitor._start: Error from getCheckSessionIframe:",t.message)}))},t.prototype._stop=function t(){this._sub=null,this._sid=null,this._checkSessionIFrame&&(i.Log.debug("SessionMonitor._stop"),this._checkSessionIFrame.stop())},t.prototype._callback=function t(){var e=this;this._userManager.querySessionStatus().then(function(t){var r=!0;t?t.sub===e._sub?(r=!1,e._checkSessionIFrame.start(t.session_state),t.sid===e._sid?i.Log.debug("SessionMonitor._callback: Same sub still logged in at OP, restarting check session iframe; session_state:",t.session_state):(i.Log.debug("SessionMonitor._callback: Same sub still logged in at OP, session state has changed, restarting check session iframe; session_state:",t.session_state),e._userManager.events._raiseUserSessionChanged())):i.Log.debug("SessionMonitor._callback: Different subject signed into OP:",t.sub):i.Log.debug("SessionMonitor._callback: Subject no longer signed into OP"),r&&(i.Log.debug("SessionMonitor._callback: SessionMonitor._callback; raising signed out event"),e._userManager.events._raiseUserSignedOut())}).catch(function(t){i.Log.debug("SessionMonitor._callback: Error calling queryCurrentSigninSession; raising signed out event",t.message),e._userManager.events._raiseUserSignedOut()})},n(t,[{key:"_settings",get:function t(){return this._userManager.settings}},{key:"_metadataService",get:function t(){return this._userManager.metadataService}},{key:"_client_id",get:function t(){return this._settings.client_id}},{key:"_checkSessionInterval",get:function t(){return this._settings.checkSessionInterval}},{key:"_stopCheckSessionOnError",get:function t(){return this._settings.stopCheckSessionOnError}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CheckSessionIFrame=void 0;var n=r(3);var i=2e3;e.CheckSessionIFrame=function(){function t(e,r,n,o){var s=!(arguments.length>4&&void 0!==arguments[4])||arguments[4];!function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this._callback=e,this._client_id=r,this._url=n,this._interval=o||i,this._stopOnError=s;var u=n.indexOf("/",n.indexOf("//")+2);this._frame_origin=n.substr(0,u),this._frame=window.document.createElement("iframe"),this._frame.style.visibility="hidden",this._frame.style.position="absolute",this._frame.style.display="none",this._frame.style.width=0,this._frame.style.height=0,this._frame.src=n}return t.prototype.load=function t(){var e=this;return new Promise(function(t){e._frame.onload=function(){t()},window.document.body.appendChild(e._frame),e._boundMessageEvent=e._message.bind(e),window.addEventListener("message",e._boundMessageEvent,!1)})},t.prototype._message=function t(e){e.origin===this._frame_origin&&e.source===this._frame.contentWindow&&("error"===e.data?(n.Log.error("CheckSessionIFrame: error message from check session op iframe"),this._stopOnError&&this.stop()):"changed"===e.data?(n.Log.debug("CheckSessionIFrame: changed message from check session op iframe"),this.stop(),this._callback()):n.Log.debug("CheckSessionIFrame: "+e.data+" message from check session op iframe"))},t.prototype.start=function t(e){var r=this;if(this._session_state!==e){n.Log.debug("CheckSessionIFrame.start"),this.stop(),this._session_state=e;var i=function t(){r._frame.contentWindow.postMessage(r._client_id+" "+r._session_state,r._frame_origin)};i(),this._timer=window.setInterval(i,this._interval)}},t.prototype.stop=function t(){this._session_state=null,this._timer&&(n.Log.debug("CheckSessionIFrame.stop"),window.clearInterval(this._timer),this._timer=null)},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenRevocationClient=void 0;var n=r(3),i=r(49),o=r(44);e.TokenRevocationClient=function(){function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o.Global.XMLHttpRequest,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.MetadataService;if(function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw n.Log.error("TokenRevocationClient.ctor: No settings provided"),new Error("No settings provided.");this._settings=e,this._XMLHttpRequestCtor=r,this._metadataService=new s(this._settings)}return t.prototype.revoke=function t(e,r){var i=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"access_token";if(!e)throw n.Log.error("TokenRevocationClient.revoke: No token provided"),new Error("No token provided.");if("access_token"!==o&&"refresh_token"!=o)throw n.Log.error("TokenRevocationClient.revoke: Invalid token type"),new Error("Invalid token type.");return this._metadataService.getRevocationEndpoint().then(function(t){if(t){n.Log.debug("TokenRevocationClient.revoke: Revoking "+o);var s=i._settings.client_id,a=i._settings.client_secret;return i._revoke(t,s,a,e,o)}if(r)throw n.Log.error("TokenRevocationClient.revoke: Revocation not supported"),new Error("Revocation not supported")})},t.prototype._revoke=function t(e,r,i,o,s){var a=this;return new Promise(function(t,u){var c=new a._XMLHttpRequestCtor;c.open("POST",e),c.onload=function(){n.Log.debug("TokenRevocationClient.revoke: HTTP response received, status",c.status),200===c.status?t():u(Error(c.statusText+" ("+c.status+")"))},c.onerror=function(){n.Log.debug("TokenRevocationClient.revoke: Network Error."),u("Network Error")};var f="client_id="+encodeURIComponent(r);i&&(f+="&client_secret="+encodeURIComponent(i)),f+="&token_type_hint="+encodeURIComponent(s),f+="&token="+encodeURIComponent(o),c.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),c.send(f)})},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CordovaPopupWindow=void 0;var n=function(){function t(t,e){for(var r=0;ro;)z(e,n=i[o++],r[n]);return e},G=function t(e){var r=L.call(this,e=b(e,!0));return!(this===B&&i(j,e)&&!i(U,e))&&(!(r||!i(this,e)||!i(j,e)||i(this,D)&&this[D][e])||r)},X=function t(e,r){if(e=S(e),r=b(r,!0),e!==B||!i(j,r)||i(U,r)){var n=P(e,r);return!n||!i(j,r)||i(e,D)&&e[D][r]||(n.enumerable=!0),n}},$=function t(e){for(var r,n=T(S(e)),o=[],s=0;n.length>s;)i(j,r=n[s++])||r==D||r==u||o.push(r);return o},Q=function t(e){for(var r,n=e===B,o=T(n?U:S(e)),s=[],a=0;o.length>a;)!i(j,r=o[a++])||n&&!i(B,r)||s.push(j[r]);return s};H||(a((R=function t(){if(this instanceof R)throw TypeError("Symbol is not a constructor!");var e=l(arguments.length>0?arguments[0]:void 0),r=function(t){this===B&&r.call(U,t),i(this,D)&&i(this[D],e)&&(this[D][e]=!1),q(this,e,w(1,t))};return o&&K&&q(B,e,{configurable:!0,set:r}),W(e)}).prototype,"toString",function t(){return this._k}),x.f=X,A.f=z,r(38).f=E.f=$,r(52).f=G,r(57).f=Q,o&&!r(30)&&a(B,"propertyIsEnumerable",G,!0),d.f=function(t){return W(p(t))}),s(s.G+s.W+s.F*!H,{Symbol:R});for(var Z="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),tt=0;Z.length>tt;)p(Z[tt++]);for(var et=k(p.store),rt=0;et.length>rt;)g(et[rt++]);s(s.S+s.F*!H,"Symbol",{for:function(t){return i(M,t+="")?M[t]:M[t]=R(t)},keyFor:function t(e){if(!J(e))throw TypeError(e+" is not a symbol!");for(var r in M)if(M[r]===e)return r},useSetter:function(){K=!0},useSimple:function(){K=!1}}),s(s.S+s.F*!H,"Object",{create:function t(e,r){return void 0===r?F(e):Y(F(e),r)},defineProperty:z,defineProperties:Y,getOwnPropertyDescriptor:X,getOwnPropertyNames:$,getOwnPropertySymbols:Q}),I&&s(s.S+s.F*(!H||c(function(){var t=R();return"[null]"!=O([t])||"{}"!=O({a:t})||"{}"!=O(Object(t))})),"JSON",{stringify:function t(e){for(var r,n,i=[e],o=1;arguments.length>o;)i.push(arguments[o++]);if(n=r=i[1],(_(r)||void 0!==e)&&!J(e))return y(r)||(r=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!J(e))return e}),i[1]=r,O.apply(I,i)}}),R.prototype[N]||r(12)(R.prototype,N,R.prototype.valueOf),h(R,"Symbol"),h(Math,"Math",!0),h(n.JSON,"JSON",!0)},function(t,e,r){t.exports=r(50)("native-function-to-string",Function.toString)},function(t,e,r){var n=r(35),i=r(57),o=r(52);t.exports=function(t){var e=n(t),r=i.f;if(r)for(var s,a=r(t),u=o.f,c=0;a.length>c;)u.call(t,s=a[c++])&&e.push(s);return e}},function(t,e,r){var n=r(0);n(n.S,"Object",{create:r(37)})},function(t,e,r){var n=r(0);n(n.S+n.F*!r(8),"Object",{defineProperty:r(9).f})},function(t,e,r){var n=r(0);n(n.S+n.F*!r(8),"Object",{defineProperties:r(106)})},function(t,e,r){var n=r(16),i=r(17).f;r(26)("getOwnPropertyDescriptor",function(){return function t(e,r){return i(n(e),r)}})},function(t,e,r){var n=r(10),i=r(18);r(26)("getPrototypeOf",function(){return function t(e){return i(n(e))}})},function(t,e,r){var n=r(10),i=r(35);r(26)("keys",function(){return function t(e){return i(n(e))}})},function(t,e,r){r(26)("getOwnPropertyNames",function(){return r(107).f})},function(t,e,r){var n=r(5),i=r(31).onFreeze;r(26)("freeze",function(t){return function e(r){return t&&n(r)?t(i(r)):r}})},function(t,e,r){var n=r(5),i=r(31).onFreeze;r(26)("seal",function(t){return function e(r){return t&&n(r)?t(i(r)):r}})},function(t,e,r){var n=r(5),i=r(31).onFreeze;r(26)("preventExtensions",function(t){return function e(r){return t&&n(r)?t(i(r)):r}})},function(t,e,r){var n=r(5);r(26)("isFrozen",function(t){return function e(r){return!n(r)||!!t&&t(r)}})},function(t,e,r){var n=r(5);r(26)("isSealed",function(t){return function e(r){return!n(r)||!!t&&t(r)}})},function(t,e,r){var n=r(5);r(26)("isExtensible",function(t){return function e(r){return!!n(r)&&(!t||t(r))}})},function(t,e,r){var n=r(0);n(n.S+n.F,"Object",{assign:r(108)})},function(t,e,r){var n=r(0);n(n.S,"Object",{is:r(109)})},function(t,e,r){var n=r(0);n(n.S,"Object",{setPrototypeOf:r(77).set})},function(t,e,r){"use strict";var n=r(46),i={};i[r(6)("toStringTag")]="z",i+""!="[object z]"&&r(13)(Object.prototype,"toString",function t(){return"[object "+n(this)+"]"},!0)},function(t,e,r){var n=r(0);n(n.P,"Function",{bind:r(110)})},function(t,e,r){var n=r(9).f,i=Function.prototype,o=/^\s*function ([^ (]*)/;"name"in i||r(8)&&n(i,"name",{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(t){return""}}})},function(t,e,r){"use strict";var n=r(5),i=r(18),o=r(6)("hasInstance"),s=Function.prototype;o in s||r(9).f(s,o,{value:function(t){if("function"!=typeof this||!n(t))return!1;if(!n(this.prototype))return t instanceof this;for(;t=i(t);)if(this.prototype===t)return!0;return!1}})},function(t,e,r){var n=r(0),i=r(112);n(n.G+n.F*(parseInt!=i),{parseInt:i})},function(t,e,r){var n=r(0),i=r(113);n(n.G+n.F*(parseFloat!=i),{parseFloat:i})},function(t,e,r){"use strict";var n=r(2),i=r(15),o=r(21),s=r(79),a=r(24),u=r(4),c=r(38).f,f=r(17).f,h=r(9).f,l=r(47).trim,p=n.Number,d=p,g=p.prototype,v="Number"==o(r(37)(g)),y="trim"in String.prototype,m=function(t){var e=a(t,!1);if("string"==typeof e&&e.length>2){var r,n,i,o=(e=y?e.trim():l(e,3)).charCodeAt(0);if(43===o||45===o){if(88===(r=e.charCodeAt(2))||120===r)return NaN}else if(48===o){switch(e.charCodeAt(1)){case 66:case 98:n=2,i=49;break;case 79:case 111:n=8,i=55;break;default:return+e}for(var s,u=e.slice(2),c=0,f=u.length;ci)return NaN;return parseInt(u,n)}}return+e};if(!p(" 0o1")||!p("0b1")||p("+0x1")){p=function t(e){var r=arguments.length<1?0:e,n=this;return n instanceof p&&(v?u(function(){g.valueOf.call(n)}):"Number"!=o(n))?s(new d(m(r)),n,p):m(r)};for(var _,S=r(8)?c(d):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),b=0;S.length>b;b++)i(d,_=S[b])&&!i(p,_)&&h(p,_,f(d,_));p.prototype=g,g.constructor=p,r(13)(n,"Number",p)}},function(t,e,r){"use strict";var n=r(0),i=r(22),o=r(114),s=r(80),a=1..toFixed,u=Math.floor,c=[0,0,0,0,0,0],f="Number.toFixed: incorrect invocation!",h=function(t,e){for(var r=-1,n=e;++r<6;)n+=t*c[r],c[r]=n%1e7,n=u(n/1e7)},l=function(t){for(var e=6,r=0;--e>=0;)r+=c[e],c[e]=u(r/t),r=r%t*1e7},p=function(){for(var t=6,e="";--t>=0;)if(""!==e||0===t||0!==c[t]){var r=String(c[t]);e=""===e?r:e+s.call("0",7-r.length)+r}return e},d=function(t,e,r){return 0===e?r:e%2==1?d(t,e-1,r*t):d(t*t,e/2,r)};n(n.P+n.F*(!!a&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!r(4)(function(){a.call({})})),"Number",{toFixed:function t(e){var r,n,a,u,c=o(this,f),g=i(e),v="",y="0";if(g<0||g>20)throw RangeError(f);if(c!=c)return"NaN";if(c<=-1e21||c>=1e21)return String(c);if(c<0&&(v="-",c=-c),c>1e-21)if(n=(r=function(t){for(var e=0,r=t;r>=4096;)e+=12,r/=4096;for(;r>=2;)e+=1,r/=2;return e}(c*d(2,69,1))-69)<0?c*d(2,-r,1):c/d(2,r,1),n*=4503599627370496,(r=52-r)>0){for(h(0,n),a=g;a>=7;)h(1e7,0),a-=7;for(h(d(10,a,1),0),a=r-1;a>=23;)l(1<<23),a-=23;l(1<0?v+((u=y.length)<=g?"0."+s.call("0",g-u)+y:y.slice(0,u-g)+"."+y.slice(u-g)):v+y}})},function(t,e,r){"use strict";var n=r(0),i=r(4),o=r(114),s=1..toPrecision;n(n.P+n.F*(i(function(){return"1"!==s.call(1,void 0)})||!i(function(){s.call({})})),"Number",{toPrecision:function t(e){var r=o(this,"Number#toPrecision: incorrect invocation!");return void 0===e?s.call(r):s.call(r,e)}})},function(t,e,r){var n=r(0);n(n.S,"Number",{EPSILON:Math.pow(2,-52)})},function(t,e,r){var n=r(0),i=r(2).isFinite;n(n.S,"Number",{isFinite:function t(e){return"number"==typeof e&&i(e)}})},function(t,e,r){var n=r(0);n(n.S,"Number",{isInteger:r(115)})},function(t,e,r){var n=r(0);n(n.S,"Number",{isNaN:function t(e){return e!=e}})},function(t,e,r){var n=r(0),i=r(115),o=Math.abs;n(n.S,"Number",{isSafeInteger:function t(e){return i(e)&&o(e)<=9007199254740991}})},function(t,e,r){var n=r(0);n(n.S,"Number",{MAX_SAFE_INTEGER:9007199254740991})},function(t,e,r){var n=r(0);n(n.S,"Number",{MIN_SAFE_INTEGER:-9007199254740991})},function(t,e,r){var n=r(0),i=r(113);n(n.S+n.F*(Number.parseFloat!=i),"Number",{parseFloat:i})},function(t,e,r){var n=r(0),i=r(112);n(n.S+n.F*(Number.parseInt!=i),"Number",{parseInt:i})},function(t,e,r){var n=r(0),i=r(116),o=Math.sqrt,s=Math.acosh;n(n.S+n.F*!(s&&710==Math.floor(s(Number.MAX_VALUE))&&s(1/0)==1/0),"Math",{acosh:function t(e){return(e=+e)<1?NaN:e>94906265.62425156?Math.log(e)+Math.LN2:i(e-1+o(e-1)*o(e+1))}})},function(t,e,r){var n=r(0),i=Math.asinh;n(n.S+n.F*!(i&&1/i(0)>0),"Math",{asinh:function t(e){return isFinite(e=+e)&&0!=e?e<0?-t(-e):Math.log(e+Math.sqrt(e*e+1)):e}})},function(t,e,r){var n=r(0),i=Math.atanh;n(n.S+n.F*!(i&&1/i(-0)<0),"Math",{atanh:function t(e){return 0==(e=+e)?e:Math.log((1+e)/(1-e))/2}})},function(t,e,r){var n=r(0),i=r(81);n(n.S,"Math",{cbrt:function t(e){return i(e=+e)*Math.pow(Math.abs(e),1/3)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{clz32:function t(e){return(e>>>=0)?31-Math.floor(Math.log(e+.5)*Math.LOG2E):32}})},function(t,e,r){var n=r(0),i=Math.exp;n(n.S,"Math",{cosh:function t(e){return(i(e=+e)+i(-e))/2}})},function(t,e,r){var n=r(0),i=r(82);n(n.S+n.F*(i!=Math.expm1),"Math",{expm1:i})},function(t,e,r){var n=r(0);n(n.S,"Math",{fround:r(117)})},function(t,e,r){var n=r(0),i=Math.abs;n(n.S,"Math",{hypot:function t(e,r){for(var n,o,s=0,a=0,u=arguments.length,c=0;a0?(o=n/c)*o:n;return c===1/0?1/0:c*Math.sqrt(s)}})},function(t,e,r){var n=r(0),i=Math.imul;n(n.S+n.F*r(4)(function(){return-5!=i(4294967295,5)||2!=i.length}),"Math",{imul:function t(e,r){var n=+e,i=+r,o=65535&n,s=65535&i;return 0|o*s+((65535&n>>>16)*s+o*(65535&i>>>16)<<16>>>0)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{log10:function t(e){return Math.log(e)*Math.LOG10E}})},function(t,e,r){var n=r(0);n(n.S,"Math",{log1p:r(116)})},function(t,e,r){var n=r(0);n(n.S,"Math",{log2:function t(e){return Math.log(e)/Math.LN2}})},function(t,e,r){var n=r(0);n(n.S,"Math",{sign:r(81)})},function(t,e,r){var n=r(0),i=r(82),o=Math.exp;n(n.S+n.F*r(4)(function(){return-2e-17!=!Math.sinh(-2e-17)}),"Math",{sinh:function t(e){return Math.abs(e=+e)<1?(i(e)-i(-e))/2:(o(e-1)-o(-e-1))*(Math.E/2)}})},function(t,e,r){var n=r(0),i=r(82),o=Math.exp;n(n.S,"Math",{tanh:function t(e){var r=i(e=+e),n=i(-e);return r==1/0?1:n==1/0?-1:(r-n)/(o(e)+o(-e))}})},function(t,e,r){var n=r(0);n(n.S,"Math",{trunc:function t(e){return(e>0?Math.floor:Math.ceil)(e)}})},function(t,e,r){var n=r(0),i=r(36),o=String.fromCharCode,s=String.fromCodePoint;n(n.S+n.F*(!!s&&1!=s.length),"String",{fromCodePoint:function t(e){for(var r,n=[],s=arguments.length,a=0;s>a;){if(r=+arguments[a++],i(r,1114111)!==r)throw RangeError(r+" is not a valid code point");n.push(r<65536?o(r):o(55296+((r-=65536)>>10),r%1024+56320))}return n.join("")}})},function(t,e,r){var n=r(0),i=r(16),o=r(7);n(n.S,"String",{raw:function t(e){for(var r=i(e.raw),n=o(r.length),s=arguments.length,a=[],u=0;n>u;)a.push(String(r[u++])),u=e.length?{value:void 0,done:!0}:(t=n(e,r),this._i+=t.length,{value:t,done:!1})})},function(t,e,r){"use strict";var n=r(0),i=r(59)(!1);n(n.P,"String",{codePointAt:function t(e){return i(this,e)}})},function(t,e,r){"use strict";var n=r(0),i=r(7),o=r(85),s="".endsWith;n(n.P+n.F*r(86)("endsWith"),"String",{endsWith:function t(e){var r=o(this,e,"endsWith"),n=arguments.length>1?arguments[1]:void 0,a=i(r.length),u=void 0===n?a:Math.min(i(n),a),c=String(e);return s?s.call(r,c,u):r.slice(u-c.length,u)===c}})},function(t,e,r){"use strict";var n=r(0),i=r(85);n(n.P+n.F*r(86)("includes"),"String",{includes:function t(e){return!!~i(this,e,"includes").indexOf(e,arguments.length>1?arguments[1]:void 0)}})},function(t,e,r){var n=r(0);n(n.P,"String",{repeat:r(80)})},function(t,e,r){"use strict";var n=r(0),i=r(7),o=r(85),s="".startsWith;n(n.P+n.F*r(86)("startsWith"),"String",{startsWith:function t(e){var r=o(this,e,"startsWith"),n=i(Math.min(arguments.length>1?arguments[1]:void 0,r.length)),a=String(e);return s?s.call(r,a,n):r.slice(n,n+a.length)===a}})},function(t,e,r){"use strict";r(14)("anchor",function(t){return function e(r){return t(this,"a","name",r)}})},function(t,e,r){"use strict";r(14)("big",function(t){return function e(){return t(this,"big","","")}})},function(t,e,r){"use strict";r(14)("blink",function(t){return function e(){return t(this,"blink","","")}})},function(t,e,r){"use strict";r(14)("bold",function(t){return function e(){return t(this,"b","","")}})},function(t,e,r){"use strict";r(14)("fixed",function(t){return function e(){return t(this,"tt","","")}})},function(t,e,r){"use strict";r(14)("fontcolor",function(t){return function e(r){return t(this,"font","color",r)}})},function(t,e,r){"use strict";r(14)("fontsize",function(t){return function e(r){return t(this,"font","size",r)}})},function(t,e,r){"use strict";r(14)("italics",function(t){return function e(){return t(this,"i","","")}})},function(t,e,r){"use strict";r(14)("link",function(t){return function e(r){return t(this,"a","href",r)}})},function(t,e,r){"use strict";r(14)("small",function(t){return function e(){return t(this,"small","","")}})},function(t,e,r){"use strict";r(14)("strike",function(t){return function e(){return t(this,"strike","","")}})},function(t,e,r){"use strict";r(14)("sub",function(t){return function e(){return t(this,"sub","","")}})},function(t,e,r){"use strict";r(14)("sup",function(t){return function e(){return t(this,"sup","","")}})},function(t,e,r){var n=r(0);n(n.S,"Date",{now:function(){return(new Date).getTime()}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(24);n(n.P+n.F*r(4)(function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})}),"Date",{toJSON:function t(e){var r=i(this),n=o(r);return"number"!=typeof n||isFinite(n)?r.toISOString():null}})},function(t,e,r){var n=r(0),i=r(234);n(n.P+n.F*(Date.prototype.toISOString!==i),"Date",{toISOString:i})},function(t,e,r){"use strict";var n=r(4),i=Date.prototype.getTime,o=Date.prototype.toISOString,s=function(t){return t>9?t:"0"+t};t.exports=n(function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-5e13-1))})||!n(function(){o.call(new Date(NaN))})?function t(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var e=this,r=e.getUTCFullYear(),n=e.getUTCMilliseconds(),o=r<0?"-":r>9999?"+":"";return o+("00000"+Math.abs(r)).slice(o?-6:-4)+"-"+s(e.getUTCMonth()+1)+"-"+s(e.getUTCDate())+"T"+s(e.getUTCHours())+":"+s(e.getUTCMinutes())+":"+s(e.getUTCSeconds())+"."+(n>99?n:"0"+s(n))+"Z"}:o},function(t,e,r){var n=Date.prototype,i=n.toString,o=n.getTime;new Date(NaN)+""!="Invalid Date"&&r(13)(n,"toString",function t(){var e=o.call(this);return e==e?i.call(this):"Invalid Date"})},function(t,e,r){var n=r(6)("toPrimitive"),i=Date.prototype;n in i||r(12)(i,n,r(237))},function(t,e,r){"use strict";var n=r(1),i=r(24);t.exports=function(t){if("string"!==t&&"number"!==t&&"default"!==t)throw TypeError("Incorrect hint");return i(n(this),"number"!=t)}},function(t,e,r){var n=r(0);n(n.S,"Array",{isArray:r(58)})},function(t,e,r){"use strict";var n=r(20),i=r(0),o=r(10),s=r(118),a=r(87),u=r(7),c=r(88),f=r(89);i(i.S+i.F*!r(61)(function(t){Array.from(t)}),"Array",{from:function t(e){var r,i,h,l,p=o(e),d="function"==typeof this?this:Array,g=arguments.length,v=g>1?arguments[1]:void 0,y=void 0!==v,m=0,_=f(p);if(y&&(v=n(v,g>2?arguments[2]:void 0,2)),void 0==_||d==Array&&a(_))for(i=new d(r=u(p.length));r>m;m++)c(i,m,y?v(p[m],m):p[m]);else for(l=_.call(p),i=new d;!(h=l.next()).done;m++)c(i,m,y?s(l,v,[h.value,m],!0):h.value);return i.length=m,i}})},function(t,e,r){"use strict";var n=r(0),i=r(88);n(n.S+n.F*r(4)(function(){function t(){}return!(Array.of.call(t)instanceof t)}),"Array",{of:function t(){for(var e=0,r=arguments.length,n=new("function"==typeof this?this:Array)(r);r>e;)i(n,e,arguments[e++]);return n.length=r,n}})},function(t,e,r){"use strict";var n=r(0),i=r(16),o=[].join;n(n.P+n.F*(r(51)!=Object||!r(23)(o)),"Array",{join:function t(e){return o.call(i(this),void 0===e?",":e)}})},function(t,e,r){"use strict";var n=r(0),i=r(76),o=r(21),s=r(36),a=r(7),u=[].slice;n(n.P+n.F*r(4)(function(){i&&u.call(i)}),"Array",{slice:function t(e,r){var n=a(this.length),i=o(this);if(r=void 0===r?n:r,"Array"==i)return u.call(this,e,r);for(var c=s(e,n),f=s(r,n),h=a(f-c),l=new Array(h),p=0;p1&&(c=Math.min(c,o(arguments[1]))),c<0&&(c=n+c);c>=0;c--)if(c in r&&r[c]===e)return c||0;return-1}})},function(t,e,r){var n=r(0);n(n.P,"Array",{copyWithin:r(120)}),r(32)("copyWithin")},function(t,e,r){var n=r(0);n(n.P,"Array",{fill:r(91)}),r(32)("fill")},function(t,e,r){"use strict";var n=r(0),i=r(27)(5),o=!0;"find"in[]&&Array(1).find(function(){o=!1}),n(n.P+n.F*o,"Array",{find:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(32)("find")},function(t,e,r){"use strict";var n=r(0),i=r(27)(6),o="findIndex",s=!0;o in[]&&Array(1)[o](function(){s=!1}),n(n.P+n.F*s,"Array",{findIndex:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(32)(o)},function(t,e,r){r(39)("Array")},function(t,e,r){var n=r(2),i=r(79),o=r(9).f,s=r(38).f,a=r(60),u=r(53),c=n.RegExp,f=c,h=c.prototype,l=/a/g,p=/a/g,d=new c(l)!==l;if(r(8)&&(!d||r(4)(function(){return p[r(6)("match")]=!1,c(l)!=l||c(p)==p||"/a/i"!=c(l,"i")}))){c=function t(e,r){var n=this instanceof c,o=a(e),s=void 0===r;return!n&&o&&e.constructor===c&&s?e:i(d?new f(o&&!s?e.source:e,r):f((o=e instanceof c)?e.source:e,o&&s?u.call(e):r),n?this:h,c)};for(var g=function(t){t in c||o(c,t,{configurable:!0,get:function(){return f[t]},set:function(e){f[t]=e}})},v=s(f),y=0;v.length>y;)g(v[y++]);h.constructor=c,c.prototype=h,r(13)(n,"RegExp",c)}r(39)("RegExp")},function(t,e,r){"use strict";r(123);var n=r(1),i=r(53),o=r(8),s=/./.toString,a=function(t){r(13)(RegExp.prototype,"toString",t,!0)};r(4)(function(){return"/a/b"!=s.call({source:"a",flags:"b"})})?a(function t(){var e=n(this);return"/".concat(e.source,"/","flags"in e?e.flags:!o&&e instanceof RegExp?i.call(e):void 0)}):"toString"!=s.name&&a(function t(){return s.call(this)})},function(t,e,r){"use strict";var n=r(1),i=r(7),o=r(94),s=r(62);r(63)("match",1,function(t,e,r,a){return[function r(n){var i=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,i):new RegExp(n)[e](String(i))},function(t){var e=a(r,t,this);if(e.done)return e.value;var u=n(t),c=String(this);if(!u.global)return s(u,c);var f=u.unicode;u.lastIndex=0;for(var h,l=[],p=0;null!==(h=s(u,c));){var d=String(h[0]);l[p]=d,""===d&&(u.lastIndex=o(c,i(u.lastIndex),f)),p++}return 0===p?null:l}]})},function(t,e,r){"use strict";var n=r(1),i=r(10),o=r(7),s=r(22),a=r(94),u=r(62),c=Math.max,f=Math.min,h=Math.floor,l=/\$([$&`']|\d\d?|<[^>]*>)/g,p=/\$([$&`']|\d\d?)/g;r(63)("replace",2,function(t,e,r,d){return[function n(i,o){var s=t(this),a=void 0==i?void 0:i[e];return void 0!==a?a.call(i,s,o):r.call(String(s),i,o)},function(t,e){var i=d(r,t,this,e);if(i.done)return i.value;var h=n(t),l=String(this),p="function"==typeof e;p||(e=String(e));var v=h.global;if(v){var y=h.unicode;h.lastIndex=0}for(var m=[];;){var _=u(h,l);if(null===_)break;if(m.push(_),!v)break;""===String(_[0])&&(h.lastIndex=a(l,o(h.lastIndex),y))}for(var S,b="",w=0,F=0;F=w&&(b+=l.slice(w,x)+T,w=x+E.length)}return b+l.slice(w)}];function g(t,e,n,o,s,a){var u=n+t.length,c=o.length,f=p;return void 0!==s&&(s=i(s),f=l),r.call(a,f,function(r,i){var a;switch(i.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,n);case"'":return e.slice(u);case"<":a=s[i.slice(1,-1)];break;default:var f=+i;if(0===f)return r;if(f>c){var l=h(f/10);return 0===l?r:l<=c?void 0===o[l-1]?i.charAt(1):o[l-1]+i.charAt(1):r}a=o[f-1]}return void 0===a?"":a})}})},function(t,e,r){"use strict";var n=r(1),i=r(109),o=r(62);r(63)("search",1,function(t,e,r,s){return[function r(n){var i=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,i):new RegExp(n)[e](String(i))},function(t){var e=s(r,t,this);if(e.done)return e.value;var a=n(t),u=String(this),c=a.lastIndex;i(c,0)||(a.lastIndex=0);var f=o(a,u);return i(a.lastIndex,c)||(a.lastIndex=c),null===f?-1:f.index}]})},function(t,e,r){"use strict";var n=r(60),i=r(1),o=r(54),s=r(94),a=r(7),u=r(62),c=r(93),f=r(4),h=Math.min,l=[].push,p=!f(function(){RegExp(4294967295,"y")});r(63)("split",2,function(t,e,r,f){var d;return d="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,e){var i=String(this);if(void 0===t&&0===e)return[];if(!n(t))return r.call(i,t,e);for(var o,s,a,u=[],f=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),h=0,p=void 0===e?4294967295:e>>>0,d=new RegExp(t.source,f+"g");(o=c.call(d,i))&&!((s=d.lastIndex)>h&&(u.push(i.slice(h,o.index)),o.length>1&&o.index=p));)d.lastIndex===o.index&&d.lastIndex++;return h===i.length?!a&&d.test("")||u.push(""):u.push(i.slice(h)),u.length>p?u.slice(0,p):u}:"0".split(void 0,0).length?function(t,e){return void 0===t&&0===e?[]:r.call(this,t,e)}:r,[function r(n,i){var o=t(this),s=void 0==n?void 0:n[e];return void 0!==s?s.call(n,o,i):d.call(String(o),n,i)},function(t,e){var n=f(d,t,this,e,d!==r);if(n.done)return n.value;var c=i(t),l=String(this),g=o(c,RegExp),v=c.unicode,y=(c.ignoreCase?"i":"")+(c.multiline?"m":"")+(c.unicode?"u":"")+(p?"y":"g"),m=new g(p?c:"^(?:"+c.source+")",y),_=void 0===e?4294967295:e>>>0;if(0===_)return[];if(0===l.length)return null===u(m,l)?[l]:[];for(var S=0,b=0,w=[];bo;)s(r[o++]);t._c=[],t._n=!1,e&&!t._h&&D(t)})}},D=function(t){y.call(u,function(){var e,r,n,i=t._v,o=N(t);if(o&&(e=S(function(){P?E.emit("unhandledRejection",i,t):(r=u.onunhandledrejection)?r({promise:t,reason:i}):(n=u.console)&&n.error&&n.error("Unhandled promise rejection",i)}),t._h=P||N(t)?2:1),t._a=void 0,o&&e.e)throw e.v})},N=function(t){return 1!==t._h&&0===(t._a||t._c).length},L=function(t){y.call(u,function(){var e;P?E.emit("rejectionHandled",t):(e=u.onrejectionhandled)&&e({promise:t,reason:t._v})})},M=function(t){var e=this;e._d||(e._d=!0,(e=e._w||e)._v=t,e._s=2,e._a||(e._a=e._c.slice()),O(e,!0))},j=function(t){var e,r=this;if(!r._d){r._d=!0,r=r._w||r;try{if(r===t)throw F("Promise can't be resolved itself");(e=I(t))?m(function(){var n={_w:r,_d:!1};try{e.call(t,c(j,n,1),c(M,n,1))}catch(t){M.call(n,t)}}):(r._v=t,r._s=1,O(r,!1))}catch(t){M.call({_w:r,_d:!1},t)}}};R||(k=function t(e){d(this,k,"Promise","_h"),p(e),n.call(this);try{e(c(j,this,1),c(M,this,1))}catch(t){M.call(this,t)}},(n=function t(e){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1}).prototype=r(42)(k.prototype,{then:function t(e,r){var n=T(v(this,k));return n.ok="function"!=typeof e||e,n.fail="function"==typeof r&&r,n.domain=P?E.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&O(this,!1),n.promise},catch:function(t){return this.then(void 0,t)}}),o=function(){var t=new n;this.promise=t,this.resolve=c(j,t,1),this.reject=c(M,t,1)},_.f=T=function(t){return t===k||t===s?new o(t):i(t)}),h(h.G+h.W+h.F*!R,{Promise:k}),r(45)(k,"Promise"),r(39)("Promise"),s=r(19).Promise,h(h.S+h.F*!R,"Promise",{reject:function t(e){var r=T(this);return(0,r.reject)(e),r.promise}}),h(h.S+h.F*(a||!R),"Promise",{resolve:function t(e){return w(a&&this===s?k:this,e)}}),h(h.S+h.F*!(R&&r(61)(function(t){k.all(t).catch(C)})),"Promise",{all:function t(e){var r=this,n=T(r),i=n.resolve,o=n.reject,s=S(function(){var t=[],n=0,s=1;g(e,!1,function(e){var a=n++,u=!1;t.push(void 0),s++,r.resolve(e).then(function(e){u||(u=!0,t[a]=e,--s||i(t))},o)}),--s||i(t)});return s.e&&o(s.v),n.promise},race:function t(e){var r=this,n=T(r),i=n.reject,o=S(function(){g(e,!1,function(t){r.resolve(t).then(n.resolve,i)})});return o.e&&i(o.v),n.promise}})},function(t,e,r){"use strict";var n=r(130),i=r(43);r(65)("WeakSet",function(t){return function e(){return t(this,arguments.length>0?arguments[0]:void 0)}},{add:function t(e){return n.def(i(this,"WeakSet"),e,!0)}},n,!1,!0)},function(t,e,r){"use strict";var n=r(0),i=r(66),o=r(98),s=r(1),a=r(36),u=r(7),c=r(5),f=r(2).ArrayBuffer,h=r(54),l=o.ArrayBuffer,p=o.DataView,d=i.ABV&&f.isView,g=l.prototype.slice,v=i.VIEW;n(n.G+n.W+n.F*(f!==l),{ArrayBuffer:l}),n(n.S+n.F*!i.CONSTR,"ArrayBuffer",{isView:function t(e){return d&&d(e)||c(e)&&v in e}}),n(n.P+n.U+n.F*r(4)(function(){return!new l(2).slice(1,void 0).byteLength}),"ArrayBuffer",{slice:function t(e,r){if(void 0!==g&&void 0===r)return g.call(s(this),e);for(var n=s(this).byteLength,i=a(e,n),o=a(void 0===r?n:r,n),c=new(h(this,l))(u(o-i)),f=new p(this),d=new p(c),v=0;i=e.length)return{value:void 0,done:!0}}while(!((t=e[this._i++])in this._t));return{value:t,done:!1}}),n(n.S,"Reflect",{enumerate:function t(e){return new o(e)}})},function(t,e,r){var n=r(17),i=r(18),o=r(15),s=r(0),a=r(5),u=r(1);s(s.S,"Reflect",{get:function t(e,r){var s,c,f=arguments.length<3?e:arguments[2];return u(e)===f?e[r]:(s=n.f(e,r))?o(s,"value")?s.value:void 0!==s.get?s.get.call(f):void 0:a(c=i(e))?t(c,r,f):void 0}})},function(t,e,r){var n=r(17),i=r(0),o=r(1);i(i.S,"Reflect",{getOwnPropertyDescriptor:function t(e,r){return n.f(o(e),r)}})},function(t,e,r){var n=r(0),i=r(18),o=r(1);n(n.S,"Reflect",{getPrototypeOf:function t(e){return i(o(e))}})},function(t,e,r){var n=r(0);n(n.S,"Reflect",{has:function t(e,r){return r in e}})},function(t,e,r){var n=r(0),i=r(1),o=Object.isExtensible;n(n.S,"Reflect",{isExtensible:function t(e){return i(e),!o||o(e)}})},function(t,e,r){var n=r(0);n(n.S,"Reflect",{ownKeys:r(132)})},function(t,e,r){var n=r(0),i=r(1),o=Object.preventExtensions;n(n.S,"Reflect",{preventExtensions:function t(e){i(e);try{return o&&o(e),!0}catch(t){return!1}}})},function(t,e,r){var n=r(9),i=r(17),o=r(18),s=r(15),a=r(0),u=r(33),c=r(1),f=r(5);a(a.S,"Reflect",{set:function t(e,r,a){var h,l,p=arguments.length<4?e:arguments[3],d=i.f(c(e),r);if(!d){if(f(l=o(e)))return t(l,r,a,p);d=u(0)}if(s(d,"value")){if(!1===d.writable||!f(p))return!1;if(h=i.f(p,r)){if(h.get||h.set||!1===h.writable)return!1;h.value=a,n.f(p,r,h)}else n.f(p,r,u(0,a));return!0}return void 0!==d.set&&(d.set.call(p,a),!0)}})},function(t,e,r){var n=r(0),i=r(77);i&&n(n.S,"Reflect",{setPrototypeOf:function t(e,r){i.check(e,r);try{return i.set(e,r),!0}catch(t){return!1}}})},function(t,e,r){"use strict";var n=r(0),i=r(56)(!0);n(n.P,"Array",{includes:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(32)("includes")},function(t,e,r){"use strict";var n=r(0),i=r(133),o=r(10),s=r(7),a=r(11),u=r(90);n(n.P,"Array",{flatMap:function t(e){var r,n,c=o(this);return a(e),r=s(c.length),n=u(c,0),i(n,c,c,r,0,1,e,arguments[1]),n}}),r(32)("flatMap")},function(t,e,r){"use strict";var n=r(0),i=r(133),o=r(10),s=r(7),a=r(22),u=r(90);n(n.P,"Array",{flatten:function t(){var e=arguments[0],r=o(this),n=s(r.length),c=u(r,0);return i(c,r,r,n,0,void 0===e?1:a(e)),c}}),r(32)("flatten")},function(t,e,r){"use strict";var n=r(0),i=r(59)(!0);n(n.P,"String",{at:function t(e){return i(this,e)}})},function(t,e,r){"use strict";var n=r(0),i=r(134),o=r(64);n(n.P+n.F*/Version\/10\.\d+(\.\d+)? Safari\//.test(o),"String",{padStart:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!0)}})},function(t,e,r){"use strict";var n=r(0),i=r(134),o=r(64);n(n.P+n.F*/Version\/10\.\d+(\.\d+)? Safari\//.test(o),"String",{padEnd:function t(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!1)}})},function(t,e,r){"use strict";r(47)("trimLeft",function(t){return function e(){return t(this,1)}},"trimStart")},function(t,e,r){"use strict";r(47)("trimRight",function(t){return function e(){return t(this,2)}},"trimEnd")},function(t,e,r){"use strict";var n=r(0),i=r(25),o=r(7),s=r(60),a=r(53),u=RegExp.prototype,c=function(t,e){this._r=t,this._s=e};r(84)(c,"RegExp String",function t(){var e=this._r.exec(this._s);return{value:e,done:null===e}}),n(n.P,"String",{matchAll:function t(e){if(i(this),!s(e))throw TypeError(e+" is not a regexp!");var r=String(this),n="flags"in u?String(e.flags):a.call(e),f=new RegExp(e.source,~n.indexOf("g")?n:"g"+n);return f.lastIndex=o(e.lastIndex),new c(f,r)}})},function(t,e,r){r(73)("asyncIterator")},function(t,e,r){r(73)("observable")},function(t,e,r){var n=r(0),i=r(132),o=r(16),s=r(17),a=r(88);n(n.S,"Object",{getOwnPropertyDescriptors:function t(e){for(var r,n,u=o(e),c=s.f,f=i(u),h={},l=0;f.length>l;)void 0!==(n=c(u,r=f[l++]))&&a(h,r,n);return h}})},function(t,e,r){var n=r(0),i=r(135)(!1);n(n.S,"Object",{values:function t(e){return i(e)}})},function(t,e,r){var n=r(0),i=r(135)(!0);n(n.S,"Object",{entries:function t(e){return i(e)}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(11),s=r(9);r(8)&&n(n.P+r(67),"Object",{__defineGetter__:function t(e,r){s.f(i(this),e,{get:o(r),enumerable:!0,configurable:!0})}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(11),s=r(9);r(8)&&n(n.P+r(67),"Object",{__defineSetter__:function t(e,r){s.f(i(this),e,{set:o(r),enumerable:!0,configurable:!0})}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(24),s=r(18),a=r(17).f;r(8)&&n(n.P+r(67),"Object",{__lookupGetter__:function t(e){var r,n=i(this),u=o(e,!0);do{if(r=a(n,u))return r.get}while(n=s(n))}})},function(t,e,r){"use strict";var n=r(0),i=r(10),o=r(24),s=r(18),a=r(17).f;r(8)&&n(n.P+r(67),"Object",{__lookupSetter__:function t(e){var r,n=i(this),u=o(e,!0);do{if(r=a(n,u))return r.set}while(n=s(n))}})},function(t,e,r){var n=r(0);n(n.P+n.R,"Map",{toJSON:r(136)("Map")})},function(t,e,r){var n=r(0);n(n.P+n.R,"Set",{toJSON:r(136)("Set")})},function(t,e,r){r(68)("Map")},function(t,e,r){r(68)("Set")},function(t,e,r){r(68)("WeakMap")},function(t,e,r){r(68)("WeakSet")},function(t,e,r){r(69)("Map")},function(t,e,r){r(69)("Set")},function(t,e,r){r(69)("WeakMap")},function(t,e,r){r(69)("WeakSet")},function(t,e,r){var n=r(0);n(n.G,{global:r(2)})},function(t,e,r){var n=r(0);n(n.S,"System",{global:r(2)})},function(t,e,r){var n=r(0),i=r(21);n(n.S,"Error",{isError:function t(e){return"Error"===i(e)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{clamp:function t(e,r,n){return Math.min(n,Math.max(r,e))}})},function(t,e,r){var n=r(0);n(n.S,"Math",{DEG_PER_RAD:Math.PI/180})},function(t,e,r){var n=r(0),i=180/Math.PI;n(n.S,"Math",{degrees:function t(e){return e*i}})},function(t,e,r){var n=r(0),i=r(138),o=r(117);n(n.S,"Math",{fscale:function t(e,r,n,s,a){return o(i(e,r,n,s,a))}})},function(t,e,r){var n=r(0);n(n.S,"Math",{iaddh:function t(e,r,n,i){var o=e>>>0,s=n>>>0;return(r>>>0)+(i>>>0)+((o&s|(o|s)&~(o+s>>>0))>>>31)|0}})},function(t,e,r){var n=r(0);n(n.S,"Math",{isubh:function t(e,r,n,i){var o=e>>>0,s=n>>>0;return(r>>>0)-(i>>>0)-((~o&s|~(o^s)&o-s>>>0)>>>31)|0}})},function(t,e,r){var n=r(0);n(n.S,"Math",{imulh:function t(e,r){var n=+e,i=+r,o=65535&n,s=65535&i,a=n>>16,u=i>>16,c=(a*s>>>0)+(o*s>>>16);return a*u+(c>>16)+((o*u>>>0)+(65535&c)>>16)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{RAD_PER_DEG:180/Math.PI})},function(t,e,r){var n=r(0),i=Math.PI/180;n(n.S,"Math",{radians:function t(e){return e*i}})},function(t,e,r){var n=r(0);n(n.S,"Math",{scale:r(138)})},function(t,e,r){var n=r(0);n(n.S,"Math",{umulh:function t(e,r){var n=+e,i=+r,o=65535&n,s=65535&i,a=n>>>16,u=i>>>16,c=(a*s>>>0)+(o*s>>>16);return a*u+(c>>>16)+((o*u>>>0)+(65535&c)>>>16)}})},function(t,e,r){var n=r(0);n(n.S,"Math",{signbit:function t(e){return(e=+e)!=e?e:0==e?1/e==1/0:e>0}})},function(t,e,r){"use strict";var n=r(0),i=r(19),o=r(2),s=r(54),a=r(125);n(n.P+n.R,"Promise",{finally:function(t){var e=s(this,i.Promise||o.Promise),r="function"==typeof t;return this.then(r?function(r){return a(e,t()).then(function(){return r})}:t,r?function(r){return a(e,t()).then(function(){throw r})}:t)}})},function(t,e,r){"use strict";var n=r(0),i=r(97),o=r(124);n(n.S,"Promise",{try:function(t){var e=i.f(this),r=o(t);return(r.e?e.reject:e.resolve)(r.v),e.promise}})},function(t,e,r){var n=r(29),i=r(1),o=n.key,s=n.set;n.exp({defineMetadata:function t(e,r,n,a){s(e,r,i(n),o(a))}})},function(t,e,r){var n=r(29),i=r(1),o=n.key,s=n.map,a=n.store;n.exp({deleteMetadata:function t(e,r){var n=arguments.length<3?void 0:o(arguments[2]),u=s(i(r),n,!1);if(void 0===u||!u.delete(e))return!1;if(u.size)return!0;var c=a.get(r);return c.delete(n),!!c.size||a.delete(r)}})},function(t,e,r){var n=r(29),i=r(1),o=r(18),s=n.has,a=n.get,u=n.key,c=function(t,e,r){if(s(t,e,r))return a(t,e,r);var n=o(e);return null!==n?c(t,n,r):void 0};n.exp({getMetadata:function t(e,r){return c(e,i(r),arguments.length<3?void 0:u(arguments[2]))}})},function(t,e,r){var n=r(128),i=r(137),o=r(29),s=r(1),a=r(18),u=o.keys,c=o.key,f=function(t,e){var r=u(t,e),o=a(t);if(null===o)return r;var s=f(o,e);return s.length?r.length?i(new n(r.concat(s))):s:r};o.exp({getMetadataKeys:function t(e){return f(s(e),arguments.length<2?void 0:c(arguments[1]))}})},function(t,e,r){var n=r(29),i=r(1),o=n.get,s=n.key;n.exp({getOwnMetadata:function t(e,r){return o(e,i(r),arguments.length<3?void 0:s(arguments[2]))}})},function(t,e,r){var n=r(29),i=r(1),o=n.keys,s=n.key;n.exp({getOwnMetadataKeys:function t(e){return o(i(e),arguments.length<2?void 0:s(arguments[1]))}})},function(t,e,r){var n=r(29),i=r(1),o=r(18),s=n.has,a=n.key,u=function(t,e,r){if(s(t,e,r))return!0;var n=o(e);return null!==n&&u(t,n,r)};n.exp({hasMetadata:function t(e,r){return u(e,i(r),arguments.length<3?void 0:a(arguments[2]))}})},function(t,e,r){var n=r(29),i=r(1),o=n.has,s=n.key;n.exp({hasOwnMetadata:function t(e,r){return o(e,i(r),arguments.length<3?void 0:s(arguments[2]))}})},function(t,e,r){var n=r(29),i=r(1),o=r(11),s=n.key,a=n.set;n.exp({metadata:function t(e,r){return function t(n,u){a(e,r,(void 0!==u?i:o)(n),s(u))}}})},function(t,e,r){var n=r(0),i=r(96)(),o=r(2).process,s="process"==r(21)(o);n(n.G,{asap:function t(e){var r=s&&o.domain;i(r?r.bind(e):e)}})},function(t,e,r){"use strict";var n=r(0),i=r(2),o=r(19),s=r(96)(),a=r(6)("observable"),u=r(11),c=r(1),f=r(40),h=r(42),l=r(12),p=r(41),d=p.RETURN,g=function(t){return null==t?void 0:u(t)},v=function(t){var e=t._c;e&&(t._c=void 0,e())},y=function(t){return void 0===t._o},m=function(t){y(t)||(t._o=void 0,v(t))},_=function(t,e){c(t),this._c=void 0,this._o=t,t=new S(this);try{var r=e(t),n=r;null!=r&&("function"==typeof r.unsubscribe?r=function(){n.unsubscribe()}:u(r),this._c=r)}catch(e){return void t.error(e)}y(this)&&v(this)};_.prototype=h({},{unsubscribe:function t(){m(this)}});var S=function(t){this._s=t};S.prototype=h({},{next:function t(e){var r=this._s;if(!y(r)){var n=r._o;try{var i=g(n.next);if(i)return i.call(n,e)}catch(t){try{m(r)}finally{throw t}}}},error:function t(e){var r=this._s;if(y(r))throw e;var n=r._o;r._o=void 0;try{var i=g(n.error);if(!i)throw e;e=i.call(n,e)}catch(t){try{v(r)}finally{throw t}}return v(r),e},complete:function t(e){var r=this._s;if(!y(r)){var n=r._o;r._o=void 0;try{var i=g(n.complete);e=i?i.call(n,e):void 0}catch(t){try{v(r)}finally{throw t}}return v(r),e}}});var b=function t(e){f(this,b,"Observable","_f")._f=u(e)};h(b.prototype,{subscribe:function t(e){return new _(e,this._f)},forEach:function t(e){var r=this;return new(o.Promise||i.Promise)(function(t,n){u(e);var i=r.subscribe({next:function(t){try{return e(t)}catch(t){n(t),i.unsubscribe()}},error:n,complete:t})})}}),h(b,{from:function t(e){var r="function"==typeof this?this:b,n=g(c(e)[a]);if(n){var i=c(n.call(e));return i.constructor===r?i:new r(function(t){return i.subscribe(t)})}return new r(function(t){var r=!1;return s(function(){if(!r){try{if(p(e,!1,function(e){if(t.next(e),r)return d})===d)return}catch(e){if(r)throw e;return void t.error(e)}t.complete()}}),function(){r=!0}})},of:function t(){for(var e=0,r=arguments.length,n=new Array(r);e2,i=!!n&&s.call(arguments,2);return t(n?function(){("function"==typeof e?e:Function(e)).apply(this,i)}:e,r)}};i(i.G+i.B+i.F*a,{setTimeout:u(n.setTimeout),setInterval:u(n.setInterval)})},function(t,e,r){var n=r(0),i=r(95);n(n.G+n.B,{setImmediate:i.set,clearImmediate:i.clear})},function(t,e,r){for(var n=r(92),i=r(35),o=r(13),s=r(2),a=r(12),u=r(48),c=r(6),f=c("iterator"),h=c("toStringTag"),l=u.Array,p={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},d=i(p),g=0;g=0;--o){var s=this.tryEntries[o],a=s.completion;if("root"===s.tryLoc)return n("end");if(s.tryLoc<=this.prev){var u=i.call(s,"catchLoc"),c=i.call(s,"finallyLoc");if(u&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),g}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var i=n.arg;C(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:R(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=r),g}}}function S(t,e,r,n){var i=e&&e.prototype instanceof w?e:w,o=Object.create(i.prototype),s=new T(n||[]);return o._invoke=function a(t,e,r){var n=h;return function i(o,s){if(n===p)throw new Error("Generator is already running");if(n===d){if("throw"===o)throw s;return I()}for(r.method=o,r.arg=s;;){var a=r.delegate;if(a){var u=k(a,r);if(u){if(u===g)continue;return u}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===h)throw n=d,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=p;var c=b(t,e,r);if("normal"===c.type){if(n=r.done?d:l,c.arg===g)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n=d,r.method="throw",r.arg=c.arg)}}}(t,r,s),o}function b(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}function w(){}function F(){}function E(){}function x(t){["next","throw","return"].forEach(function(e){t[e]=function(t){return this._invoke(e,t)}})}function A(t){function r(e,n,o,s){var a=b(t[e],t,n);if("throw"!==a.type){var u=a.arg,c=u.value;return c&&"object"==typeof c&&i.call(c,"__await")?Promise.resolve(c.__await).then(function(t){r("next",t,o,s)},function(t){r("throw",t,o,s)}):Promise.resolve(c).then(function(t){u.value=t,o(u)},s)}s(a.arg)}var n;"object"==typeof e.process&&e.process.domain&&(r=e.process.domain.bind(r)),this._invoke=function o(t,e){function i(){return new Promise(function(n,i){r(t,e,n,i)})}return n=n?n.then(i,i):i()}}function k(t,e){var n=t.iterator[e.method];if(n===r){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=r,k(t,e),"throw"===e.method))return g;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return g}var i=b(n,t.iterator,e.arg);if("throw"===i.type)return e.method="throw",e.arg=i.arg,e.delegate=null,g;var o=i.arg;return o?o.done?(e[t.resultName]=o.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=r),e.delegate=null,g):o:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,g)}function P(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function T(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(P,this),this.reset(!0)}function R(t){if(t){var e=t[s];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,o=function e(){for(;++n1&&void 0!==arguments[1]?arguments[1]:o.MetadataService,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:s.UserInfoService,u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:c.JoseUtil,f=arguments.length>4&&void 0!==arguments[4]?arguments[4]:a.TokenClient;if(function h(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw i.Log.error("ResponseValidator.ctor: No settings passed to ResponseValidator"),new Error("settings");this._settings=e,this._metadataService=new r(this._settings),this._userInfoService=new n(this._settings),this._joseUtil=u,this._tokenClient=new f(this._settings)}return t.prototype.validateSigninResponse=function t(e,r){var n=this;return i.Log.debug("ResponseValidator.validateSigninResponse"),this._processSigninParams(e,r).then(function(t){return i.Log.debug("ResponseValidator.validateSigninResponse: state processed"),n._validateTokens(e,t).then(function(t){return i.Log.debug("ResponseValidator.validateSigninResponse: tokens validated"),n._processClaims(e,t).then(function(t){return i.Log.debug("ResponseValidator.validateSigninResponse: claims processed"),t})})})},t.prototype.validateSignoutResponse=function t(e,r){return e.id!==r.state?(i.Log.error("ResponseValidator.validateSignoutResponse: State does not match"),Promise.reject(new Error("State does not match"))):(i.Log.debug("ResponseValidator.validateSignoutResponse: state validated"),r.state=e.data,r.error?(i.Log.warn("ResponseValidator.validateSignoutResponse: Response was error",r.error),Promise.reject(new u.ErrorResponse(r))):Promise.resolve(r))},t.prototype._processSigninParams=function t(e,r){if(e.id!==r.state)return i.Log.error("ResponseValidator._processSigninParams: State does not match"),Promise.reject(new Error("State does not match"));if(!e.client_id)return i.Log.error("ResponseValidator._processSigninParams: No client_id on state"),Promise.reject(new Error("No client_id on state"));if(!e.authority)return i.Log.error("ResponseValidator._processSigninParams: No authority on state"),Promise.reject(new Error("No authority on state"));if(this._settings.authority){if(this._settings.authority&&this._settings.authority!==e.authority)return i.Log.error("ResponseValidator._processSigninParams: authority mismatch on settings vs. signin state"),Promise.reject(new Error("authority mismatch on settings vs. signin state"))}else this._settings.authority=e.authority;if(this._settings.client_id){if(this._settings.client_id&&this._settings.client_id!==e.client_id)return i.Log.error("ResponseValidator._processSigninParams: client_id mismatch on settings vs. signin state"),Promise.reject(new Error("client_id mismatch on settings vs. signin state"))}else this._settings.client_id=e.client_id;return i.Log.debug("ResponseValidator._processSigninParams: state validated"),r.state=e.data,r.error?(i.Log.warn("ResponseValidator._processSigninParams: Response was error",r.error),Promise.reject(new u.ErrorResponse(r))):e.nonce&&!r.id_token?(i.Log.error("ResponseValidator._processSigninParams: Expecting id_token in response"),Promise.reject(new Error("No id_token in response"))):!e.nonce&&r.id_token?(i.Log.error("ResponseValidator._processSigninParams: Not expecting id_token in response"),Promise.reject(new Error("Unexpected id_token in response"))):e.code_verifier&&!r.code?(i.Log.error("ResponseValidator._processSigninParams: Expecting code in response"),Promise.reject(new Error("No code in response"))):!e.code_verifier&&r.code?(i.Log.error("ResponseValidator._processSigninParams: Not expecting code in response"),Promise.reject(new Error("Unexpected code in response"))):(r.scope||(r.scope=e.scope),Promise.resolve(r))},t.prototype._processClaims=function t(e,r){var n=this;if(r.isOpenIdConnect){if(i.Log.debug("ResponseValidator._processClaims: response is OIDC, processing claims"),r.profile=this._filterProtocolClaims(r.profile),!0!==e.skipUserInfo&&this._settings.loadUserInfo&&r.access_token)return i.Log.debug("ResponseValidator._processClaims: loading user info"),this._userInfoService.getClaims(r.access_token).then(function(t){return i.Log.debug("ResponseValidator._processClaims: user info claims received from user info endpoint"),t.sub!==r.profile.sub?(i.Log.error("ResponseValidator._processClaims: sub from user info endpoint does not match sub in access_token"),Promise.reject(new Error("sub from user info endpoint does not match sub in access_token"))):(r.profile=n._mergeClaims(r.profile,t),i.Log.debug("ResponseValidator._processClaims: user info claims received, updated profile:",r.profile),r)});i.Log.debug("ResponseValidator._processClaims: not loading user info")}else i.Log.debug("ResponseValidator._processClaims: response is not OIDC, not processing claims");return Promise.resolve(r)},t.prototype._mergeClaims=function t(e,r){var i=Object.assign({},e);for(var o in r){var s=r[o];Array.isArray(s)||(s=[s]);for(var a=0;a1)return i.Log.error("ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata"),Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));u=a[0]}if(!u)return i.Log.error("ResponseValidator._validateIdToken: No key matching kid or alg found in signing keys"),Promise.reject(new Error("No key matching kid or alg found in signing keys"));var c=e.client_id,f=n._settings.clockSkew;return i.Log.debug("ResponseValidator._validateIdToken: Validaing JWT; using clock skew (in seconds) of: ",f),n._joseUtil.validateJwt(r.id_token,u,t,c,f).then(function(){return i.Log.debug("ResponseValidator._validateIdToken: JWT validation successful"),o.payload.sub?(r.profile=o.payload,r):(i.Log.error("ResponseValidator._validateIdToken: No sub present in id_token"),Promise.reject(new Error("No sub present in id_token")))})})})},t.prototype._filterByAlg=function t(e,r){var n=null;if(r.startsWith("RS"))n="RSA";else if(r.startsWith("PS"))n="PS";else{if(!r.startsWith("ES"))return i.Log.debug("ResponseValidator._filterByAlg: alg not supported: ",r),[];n="EC"}return i.Log.debug("ResponseValidator._filterByAlg: Looking for keys that match kty: ",n),e=e.filter(function(t){return t.kty===n}),i.Log.debug("ResponseValidator._filterByAlg: Number of keys that match kty: ",n,e.length),e},t.prototype._validateAccessToken=function t(e){if(!e.profile)return i.Log.error("ResponseValidator._validateAccessToken: No profile loaded from id_token"),Promise.reject(new Error("No profile loaded from id_token"));if(!e.profile.at_hash)return i.Log.error("ResponseValidator._validateAccessToken: No at_hash in id_token"),Promise.reject(new Error("No at_hash in id_token"));if(!e.id_token)return i.Log.error("ResponseValidator._validateAccessToken: No id_token"),Promise.reject(new Error("No id_token"));var r=this._joseUtil.parseJwt(e.id_token);if(!r||!r.header)return i.Log.error("ResponseValidator._validateAccessToken: Failed to parse id_token",r),Promise.reject(new Error("Failed to parse id_token"));var n=r.header.alg;if(!n||5!==n.length)return i.Log.error("ResponseValidator._validateAccessToken: Unsupported alg:",n),Promise.reject(new Error("Unsupported alg: "+n));var o=n.substr(2,3);if(!o)return i.Log.error("ResponseValidator._validateAccessToken: Unsupported alg:",n,o),Promise.reject(new Error("Unsupported alg: "+n));if(256!==(o=parseInt(o))&&384!==o&&512!==o)return i.Log.error("ResponseValidator._validateAccessToken: Unsupported alg:",n,o),Promise.reject(new Error("Unsupported alg: "+n));var s="sha"+o,a=this._joseUtil.hashString(e.access_token,s);if(!a)return i.Log.error("ResponseValidator._validateAccessToken: access_token hash failed:",s),Promise.reject(new Error("Failed to validate at_hash"));var u=a.substr(0,a.length/2),c=this._joseUtil.hexToBase64Url(u);return c!==e.profile.at_hash?(i.Log.error("ResponseValidator._validateAccessToken: Failed to validate at_hash",c,e.profile.at_hash),Promise.reject(new Error("Failed to validate at_hash"))):(i.Log.debug("ResponseValidator._validateAccessToken: success"),Promise.resolve(e))},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UserInfoService=void 0;var n=r(101),i=r(49),o=r(3),s=r(70);e.UserInfoService=function(){function t(e){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n.JsonService,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:i.MetadataService,u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:s.JoseUtil;if(function c(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!e)throw o.Log.error("UserInfoService.ctor: No settings passed"),new Error("settings");this._settings=e,this._jsonService=new r(void 0,void 0,this._getClaimsFromJwt.bind(this)),this._metadataService=new a(this._settings),this._joseUtil=u}return t.prototype.getClaims=function t(e){var r=this;return e?this._metadataService.getUserInfoEndpoint().then(function(t){return o.Log.debug("UserInfoService.getClaims: received userinfo url",t),r._jsonService.getJson(t,e).then(function(t){return o.Log.debug("UserInfoService.getClaims: claims received",t),t})}):(o.Log.error("UserInfoService.getClaims: No token passed"),Promise.reject(new Error("A token is required")))},t.prototype._getClaimsFromJwt=function t(e){var r=this;try{var n=this._joseUtil.parseJwt(e.responseText);if(!n||!n.header||!n.payload)return o.Log.error("UserInfoService._getClaimsFromJwt: Failed to parse JWT",n),Promise.reject(new Error("Failed to parse id_token"));var i=n.header.kid,s=void 0;switch(this._settings.userInfoJwtIssuer){case"OP":s=this._metadataService.getIssuer();break;case"ANY":s=Promise.resolve(n.payload.iss);break;default:s=Promise.resolve(this._settings.userInfoJwtIssuer)}return s.then(function(t){return o.Log.debug("UserInfoService._getClaimsFromJwt: Received issuer:"+t),r._metadataService.getSigningKeys().then(function(s){if(!s)return o.Log.error("UserInfoService._getClaimsFromJwt: No signing keys from metadata"),Promise.reject(new Error("No signing keys from metadata"));o.Log.debug("UserInfoService._getClaimsFromJwt: Received signing keys");var a=void 0;if(i)a=s.filter(function(t){return t.kid===i})[0];else{if((s=r._filterByAlg(s,n.header.alg)).length>1)return o.Log.error("UserInfoService._getClaimsFromJwt: No kid found in id_token and more than one key found in metadata"),Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));a=s[0]}if(!a)return o.Log.error("UserInfoService._getClaimsFromJwt: No key matching kid or alg found in signing keys"),Promise.reject(new Error("No key matching kid or alg found in signing keys"));var u=r._settings.client_id,c=r._settings.clockSkew;return o.Log.debug("UserInfoService._getClaimsFromJwt: Validaing JWT; using clock skew (in seconds) of: ",c),r._joseUtil.validateJwt(e.responseText,a,t,u,c,void 0,!0).then(function(){return o.Log.debug("UserInfoService._getClaimsFromJwt: JWT validation successful"),n.payload})})})}catch(t){return o.Log.error("UserInfoService._getClaimsFromJwt: Error parsing JWT response",t.message),void reject(t)}},t.prototype._filterByAlg=function t(e,r){var n=null;if(r.startsWith("RS"))n="RSA";else if(r.startsWith("PS"))n="PS";else{if(!r.startsWith("ES"))return o.Log.debug("UserInfoService._filterByAlg: alg not supported: ",r),[];n="EC"}return o.Log.debug("UserInfoService._filterByAlg: Looking for keys that match kty: ",n),e=e.filter(function(t){return t.kty===n}),o.Log.debug("UserInfoService._filterByAlg: Number of keys that match kty: ",n,e.length),e},t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AllowedSigningAlgs=e.b64tohex=e.hextob64u=e.crypto=e.X509=e.KeyUtil=e.jws=void 0;var n=r(359);e.jws=n.jws,e.KeyUtil=n.KEYUTIL,e.X509=n.X509,e.crypto=n.crypto,e.hextob64u=n.hextob64u,e.b64tohex=n.b64tohex,e.AllowedSigningAlgs=["RS256","RS384","RS512","PS256","PS384","PS512","ES256","ES384","ES512"]},function(t,e,r){"use strict";(function(t){Object.defineProperty(e,"__esModule",{value:!0});var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n={userAgent:!1},i={}; +/*! +Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.9.0 +*/ +if(void 0===o)var o={};o.lang={extend:function t(e,r,i){if(!r||!e)throw new Error("YAHOO.lang.extend failed, please check that all dependencies are included.");var o=function t(){};if(o.prototype=r.prototype,e.prototype=new o,e.prototype.constructor=e,e.superclass=r.prototype,r.prototype.constructor==Object.prototype.constructor&&(r.prototype.constructor=r),i){var s;for(s in i)e.prototype[s]=i[s];var a=function t(){},u=["toString","valueOf"];try{/MSIE/.test(n.userAgent)&&(a=function t(e,r){for(s=0;s>>2]>>>24-s%4*8&255;r[i+s>>>2]|=a<<24-(i+s)%4*8}else for(s=0;s>>2]=n[s>>>2];return this.sigBytes+=o,this},clamp:function t(){var e=this.words,r=this.sigBytes;e[r>>>2]&=4294967295<<32-r%4*8,e.length=s.ceil(r/4)},clone:function t(){var e=c.clone.call(this);return e.words=this.words.slice(0),e},random:function t(e){for(var r=[],n=0;n>>2]>>>24-o%4*8&255;i.push((s>>>4).toString(16)),i.push((15&s).toString(16))}return i.join("")},parse:function t(e){for(var r=e.length,n=[],i=0;i>>3]|=parseInt(e.substr(i,2),16)<<24-i%8*4;return new f.init(n,r/2)}},p=h.Latin1={stringify:function t(e){for(var r=e.words,n=e.sigBytes,i=[],o=0;o>>2]>>>24-o%4*8&255;i.push(String.fromCharCode(s))}return i.join("")},parse:function t(e){for(var r=e.length,n=[],i=0;i>>2]|=(255&e.charCodeAt(i))<<24-i%4*8;return new f.init(n,r)}},d=h.Utf8={stringify:function t(e){try{return decodeURIComponent(escape(p.stringify(e)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function t(e){return p.parse(unescape(encodeURIComponent(e)))}},g=u.BufferedBlockAlgorithm=c.extend({reset:function t(){this._data=new f.init,this._nDataBytes=0},_append:function t(e){"string"==typeof e&&(e=d.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function t(e){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,a=i/(4*o),u=(a=e?s.ceil(a):s.max((0|a)-this._minBufferSize,0))*o,c=s.min(4*u,i);if(u){for(var h=0;h>>2]>>>24-o%4*8&255)<<16|(r[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|r[o+2>>>2]>>>24-(o+2)%4*8&255,a=0;4>a&&o+.75*a>>6*(3-a)&63));if(r=i.charAt(64))for(;e.length%4;)e.push(r);return e.join("")},parse:function t(r){var n=r.length,i=this._map;(o=i.charAt(64))&&(-1!=(o=r.indexOf(o))&&(n=o));for(var o=[],s=0,a=0;a>>6-a%4*2;o[s>>>2]|=(u|c)<<24-s%4*8,s++}return e.create(o,s)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),function(t){for(var e=y,r=(i=e.lib).WordArray,n=i.Hasher,i=e.algo,o=[],s=[],a=function t(e){return 4294967296*(e-(0|e))|0},u=2,c=0;64>c;){var f;t:{f=u;for(var h=t.sqrt(f),l=2;l<=h;l++)if(!(f%l)){f=!1;break t}f=!0}f&&(8>c&&(o[c]=a(t.pow(u,.5))),s[c]=a(t.pow(u,1/3)),c++),u++}var p=[];i=i.SHA256=n.extend({_doReset:function t(){this._hash=new r.init(o.slice(0))},_doProcessBlock:function t(e,r){for(var n=this._hash.words,i=n[0],o=n[1],a=n[2],u=n[3],c=n[4],f=n[5],h=n[6],l=n[7],d=0;64>d;d++){if(16>d)p[d]=0|e[r+d];else{var g=p[d-15],v=p[d-2];p[d]=((g<<25|g>>>7)^(g<<14|g>>>18)^g>>>3)+p[d-7]+((v<<15|v>>>17)^(v<<13|v>>>19)^v>>>10)+p[d-16]}g=l+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&f^~c&h)+s[d]+p[d],v=((i<<30|i>>>2)^(i<<19|i>>>13)^(i<<10|i>>>22))+(i&o^i&a^o&a),l=h,h=f,f=c,c=u+g|0,u=a,a=o,o=i,i=g+v|0}n[0]=n[0]+i|0,n[1]=n[1]+o|0,n[2]=n[2]+a|0,n[3]=n[3]+u|0,n[4]=n[4]+c|0,n[5]=n[5]+f|0,n[6]=n[6]+h|0,n[7]=n[7]+l|0},_doFinalize:function e(){var r=this._data,n=r.words,i=8*this._nDataBytes,o=8*r.sigBytes;return n[o>>>5]|=128<<24-o%32,n[14+(o+64>>>9<<4)]=t.floor(i/4294967296),n[15+(o+64>>>9<<4)]=i,r.sigBytes=4*n.length,this._process(),this._hash},clone:function t(){var e=n.clone.call(this);return e._hash=this._hash.clone(),e}});e.SHA256=n._createHelper(i),e.HmacSHA256=n._createHmacHelper(i)}(Math),function(){function t(){return n.create.apply(n,arguments)}for(var e=y,r=e.lib.Hasher,n=(o=e.x64).Word,i=o.WordArray,o=e.algo,s=[t(1116352408,3609767458),t(1899447441,602891725),t(3049323471,3964484399),t(3921009573,2173295548),t(961987163,4081628472),t(1508970993,3053834265),t(2453635748,2937671579),t(2870763221,3664609560),t(3624381080,2734883394),t(310598401,1164996542),t(607225278,1323610764),t(1426881987,3590304994),t(1925078388,4068182383),t(2162078206,991336113),t(2614888103,633803317),t(3248222580,3479774868),t(3835390401,2666613458),t(4022224774,944711139),t(264347078,2341262773),t(604807628,2007800933),t(770255983,1495990901),t(1249150122,1856431235),t(1555081692,3175218132),t(1996064986,2198950837),t(2554220882,3999719339),t(2821834349,766784016),t(2952996808,2566594879),t(3210313671,3203337956),t(3336571891,1034457026),t(3584528711,2466948901),t(113926993,3758326383),t(338241895,168717936),t(666307205,1188179964),t(773529912,1546045734),t(1294757372,1522805485),t(1396182291,2643833823),t(1695183700,2343527390),t(1986661051,1014477480),t(2177026350,1206759142),t(2456956037,344077627),t(2730485921,1290863460),t(2820302411,3158454273),t(3259730800,3505952657),t(3345764771,106217008),t(3516065817,3606008344),t(3600352804,1432725776),t(4094571909,1467031594),t(275423344,851169720),t(430227734,3100823752),t(506948616,1363258195),t(659060556,3750685593),t(883997877,3785050280),t(958139571,3318307427),t(1322822218,3812723403),t(1537002063,2003034995),t(1747873779,3602036899),t(1955562222,1575990012),t(2024104815,1125592928),t(2227730452,2716904306),t(2361852424,442776044),t(2428436474,593698344),t(2756734187,3733110249),t(3204031479,2999351573),t(3329325298,3815920427),t(3391569614,3928383900),t(3515267271,566280711),t(3940187606,3454069534),t(4118630271,4000239992),t(116418474,1914138554),t(174292421,2731055270),t(289380356,3203993006),t(460393269,320620315),t(685471733,587496836),t(852142971,1086792851),t(1017036298,365543100),t(1126000580,2618297676),t(1288033470,3409855158),t(1501505948,4234509866),t(1607167915,987167468),t(1816402316,1246189591)],a=[],u=0;80>u;u++)a[u]=t();o=o.SHA512=r.extend({_doReset:function t(){this._hash=new i.init([new n.init(1779033703,4089235720),new n.init(3144134277,2227873595),new n.init(1013904242,4271175723),new n.init(2773480762,1595750129),new n.init(1359893119,2917565137),new n.init(2600822924,725511199),new n.init(528734635,4215389547),new n.init(1541459225,327033209)])},_doProcessBlock:function t(e,r){for(var n=(l=this._hash.words)[0],i=l[1],o=l[2],u=l[3],c=l[4],f=l[5],h=l[6],l=l[7],p=n.high,d=n.low,g=i.high,v=i.low,y=o.high,m=o.low,_=u.high,S=u.low,b=c.high,w=c.low,F=f.high,E=f.low,x=h.high,A=h.low,k=l.high,P=l.low,C=p,T=d,R=g,I=v,O=y,D=m,N=_,L=S,M=b,j=w,U=F,B=E,H=x,V=A,K=k,q=P,W=0;80>W;W++){var J=a[W];if(16>W)var z=J.high=0|e[r+2*W],Y=J.low=0|e[r+2*W+1];else{z=((Y=(z=a[W-15]).high)>>>1|(G=z.low)<<31)^(Y>>>8|G<<24)^Y>>>7;var G=(G>>>1|Y<<31)^(G>>>8|Y<<24)^(G>>>7|Y<<25),X=((Y=(X=a[W-2]).high)>>>19|($=X.low)<<13)^(Y<<3|$>>>29)^Y>>>6,$=($>>>19|Y<<13)^($<<3|Y>>>29)^($>>>6|Y<<26),Q=(Y=a[W-7]).high,Z=(tt=a[W-16]).high,tt=tt.low;z=(z=(z=z+Q+((Y=G+Y.low)>>>0>>0?1:0))+X+((Y=Y+$)>>>0<$>>>0?1:0))+Z+((Y=Y+tt)>>>0>>0?1:0);J.high=z,J.low=Y}Q=M&U^~M&H,tt=j&B^~j&V,J=C&R^C&O^R&O;var et=T&I^T&D^I&D,rt=(G=(C>>>28|T<<4)^(C<<30|T>>>2)^(C<<25|T>>>7),X=(T>>>28|C<<4)^(T<<30|C>>>2)^(T<<25|C>>>7),($=s[W]).high),nt=$.low;Z=(Z=(Z=(Z=K+((M>>>14|j<<18)^(M>>>18|j<<14)^(M<<23|j>>>9))+(($=q+((j>>>14|M<<18)^(j>>>18|M<<14)^(j<<23|M>>>9)))>>>0>>0?1:0))+Q+(($=$+tt)>>>0>>0?1:0))+rt+(($=$+nt)>>>0>>0?1:0))+z+(($=$+Y)>>>0>>0?1:0),J=G+J+((Y=X+et)>>>0>>0?1:0),K=H,q=V,H=U,V=B,U=M,B=j,M=N+Z+((j=L+$|0)>>>0>>0?1:0)|0,N=O,L=D,O=R,D=I,R=C,I=T,C=Z+J+((T=$+Y|0)>>>0<$>>>0?1:0)|0}d=n.low=d+T,n.high=p+C+(d>>>0>>0?1:0),v=i.low=v+I,i.high=g+R+(v>>>0>>0?1:0),m=o.low=m+D,o.high=y+O+(m>>>0>>0?1:0),S=u.low=S+L,u.high=_+N+(S>>>0>>0?1:0),w=c.low=w+j,c.high=b+M+(w>>>0>>0?1:0),E=f.low=E+B,f.high=F+U+(E>>>0>>0?1:0),A=h.low=A+V,h.high=x+H+(A>>>0>>0?1:0),P=l.low=P+q,l.high=k+K+(P>>>0>>0?1:0)},_doFinalize:function t(){var e=this._data,r=e.words,n=8*this._nDataBytes,i=8*e.sigBytes;return r[i>>>5]|=128<<24-i%32,r[30+(i+128>>>10<<5)]=Math.floor(n/4294967296),r[31+(i+128>>>10<<5)]=n,e.sigBytes=4*r.length,this._process(),this._hash.toX32()},clone:function t(){var e=r.clone.call(this);return e._hash=this._hash.clone(),e},blockSize:32}),e.SHA512=r._createHelper(o),e.HmacSHA512=r._createHmacHelper(o)}(),function(){var t=y,e=(i=t.x64).Word,r=i.WordArray,n=(i=t.algo).SHA512,i=i.SHA384=n.extend({_doReset:function t(){this._hash=new r.init([new e.init(3418070365,3238371032),new e.init(1654270250,914150663),new e.init(2438529370,812702999),new e.init(355462360,4144912697),new e.init(1731405415,4290775857),new e.init(2394180231,1750603025),new e.init(3675008525,1694076839),new e.init(1203062813,3204075428)])},_doFinalize:function t(){var e=n._doFinalize.call(this);return e.sigBytes-=16,e}});t.SHA384=n._createHelper(i),t.HmacSHA384=n._createHmacHelper(i)}(); +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +var m,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",S="=";function b(t){var e,r,n="";for(e=0;e+3<=t.length;e+=3)r=parseInt(t.substring(e,e+3),16),n+=_.charAt(r>>6)+_.charAt(63&r);if(e+1==t.length?(r=parseInt(t.substring(e,e+1),16),n+=_.charAt(r<<2)):e+2==t.length&&(r=parseInt(t.substring(e,e+2),16),n+=_.charAt(r>>2)+_.charAt((3&r)<<4)),S)for(;(3&n.length)>0;)n+=S;return n}function w(t){var e,r,n,i="",o=0;for(e=0;e>2),r=3&n,o=1):1==o?(i+=O(r<<2|n>>4),r=15&n,o=2):2==o?(i+=O(r),i+=O(n>>2),r=3&n,o=3):(i+=O(r<<2|n>>4),i+=O(15&n),o=0));return 1==o&&(i+=O(r<<2)),i}function F(t){var e,r=w(t),n=new Array;for(e=0;2*e>15;--o>=0;){var u=32767&this[t],c=this[t++]>>15,f=a*u+c*s;i=((u=s*u+((32767&f)<<15)+r[n]+(1073741823&i))>>>30)+(f>>>15)+a*c+(i>>>30),r[n++]=1073741823&u}return i},m=30):"Netscape"!=n.appName?(E.prototype.am=function k(t,e,r,n,i,o){for(;--o>=0;){var s=e*this[t++]+r[n]+i;i=Math.floor(s/67108864),r[n++]=67108863&s}return i},m=26):(E.prototype.am=function P(t,e,r,n,i,o){for(var s=16383&e,a=e>>14;--o>=0;){var u=16383&this[t],c=this[t++]>>14,f=a*u+c*s;i=((u=s*u+((16383&f)<<14)+r[n]+i)>>28)+(f>>14)+a*c,r[n++]=268435455&u}return i},m=28),E.prototype.DB=m,E.prototype.DM=(1<>>16)&&(t=e,r+=16),0!=(e=t>>8)&&(t=e,r+=8),0!=(e=t>>4)&&(t=e,r+=4),0!=(e=t>>2)&&(t=e,r+=2),0!=(e=t>>1)&&(t=e,r+=1),r}function M(t){this.m=t}function j(t){this.m=t,this.mp=t.invDigit(),this.mpl=32767&this.mp,this.mph=this.mp>>15,this.um=(1<>=16,e+=16),0==(255&t)&&(t>>=8,e+=8),0==(15&t)&&(t>>=4,e+=4),0==(3&t)&&(t>>=2,e+=2),0==(1&t)&&++e,e}function q(t){for(var e=0;0!=t;)t&=t-1,++e;return e}function W(){}function J(t){return t}function z(t){this.r2=x(),this.q3=x(),E.ONE.dlShiftTo(2*t.t,this.r2),this.mu=this.r2.divide(t),this.m=t}M.prototype.convert=function Y(t){return t.s<0||t.compareTo(this.m)>=0?t.mod(this.m):t},M.prototype.revert=function G(t){return t},M.prototype.reduce=function X(t){t.divRemTo(this.m,null,t)},M.prototype.mulTo=function $(t,e,r){t.multiplyTo(e,r),this.reduce(r)},M.prototype.sqrTo=function Q(t,e){t.squareTo(e),this.reduce(e)},j.prototype.convert=function Z(t){var e=x();return t.abs().dlShiftTo(this.m.t,e),e.divRemTo(this.m,null,e),t.s<0&&e.compareTo(E.ZERO)>0&&this.m.subTo(e,e),e},j.prototype.revert=function tt(t){var e=x();return t.copyTo(e),this.reduce(e),e},j.prototype.reduce=function et(t){for(;t.t<=this.mt2;)t[t.t++]=0;for(var e=0;e>15)*this.mpl&this.um)<<15)&t.DM;for(t[r=e+this.m.t]+=this.m.am(0,n,t,e,0,this.m.t);t[r]>=t.DV;)t[r]-=t.DV,t[++r]++}t.clamp(),t.drShiftTo(this.m.t,t),t.compareTo(this.m)>=0&&t.subTo(this.m,t)},j.prototype.mulTo=function rt(t,e,r){t.multiplyTo(e,r),this.reduce(r)},j.prototype.sqrTo=function nt(t,e){t.squareTo(e),this.reduce(e)},E.prototype.copyTo=function it(t){for(var e=this.t-1;e>=0;--e)t[e]=this[e];t.t=this.t,t.s=this.s},E.prototype.fromInt=function ot(t){this.t=1,this.s=t<0?-1:0,t>0?this[0]=t:t<-1?this[0]=t+this.DV:this.t=0},E.prototype.fromString=function st(t,e){var r;if(16==e)r=4;else if(8==e)r=3;else if(256==e)r=8;else if(2==e)r=1;else if(32==e)r=5;else{if(4!=e)return void this.fromRadix(t,e);r=2}this.t=0,this.s=0;for(var n=t.length,i=!1,o=0;--n>=0;){var s=8==r?255&t[n]:D(t,n);s<0?"-"==t.charAt(n)&&(i=!0):(i=!1,0==o?this[this.t++]=s:o+r>this.DB?(this[this.t-1]|=(s&(1<>this.DB-o):this[this.t-1]|=s<=this.DB&&(o-=this.DB))}8==r&&0!=(128&t[0])&&(this.s=-1,o>0&&(this[this.t-1]|=(1<0&&this[this.t-1]==t;)--this.t},E.prototype.dlShiftTo=function ut(t,e){var r;for(r=this.t-1;r>=0;--r)e[r+t]=this[r];for(r=t-1;r>=0;--r)e[r]=0;e.t=this.t+t,e.s=this.s},E.prototype.drShiftTo=function ct(t,e){for(var r=t;r=0;--r)e[r+s+1]=this[r]>>i|a,a=(this[r]&o)<=0;--r)e[r]=0;e[s]=a,e.t=this.t+s+1,e.s=this.s,e.clamp()},E.prototype.rShiftTo=function ht(t,e){e.s=this.s;var r=Math.floor(t/this.DB);if(r>=this.t)e.t=0;else{var n=t%this.DB,i=this.DB-n,o=(1<>n;for(var s=r+1;s>n;n>0&&(e[this.t-r-1]|=(this.s&o)<>=this.DB;if(t.t>=this.DB;n+=this.s}else{for(n+=this.s;r>=this.DB;n-=t.s}e.s=n<0?-1:0,n<-1?e[r++]=this.DV+n:n>0&&(e[r++]=n),e.t=r,e.clamp()},E.prototype.multiplyTo=function pt(t,e){var r=this.abs(),n=t.abs(),i=r.t;for(e.t=i+n.t;--i>=0;)e[i]=0;for(i=0;i=0;)t[r]=0;for(r=0;r=e.DV&&(t[r+e.t]-=e.DV,t[r+e.t+1]=1)}t.t>0&&(t[t.t-1]+=e.am(r,e[r],t,2*r,0,1)),t.s=0,t.clamp()},E.prototype.divRemTo=function gt(t,e,r){var n=t.abs();if(!(n.t<=0)){var i=this.abs();if(i.t0?(n.lShiftTo(u,o),i.lShiftTo(u,r)):(n.copyTo(o),i.copyTo(r));var c=o.t,f=o[c-1];if(0!=f){var h=f*(1<1?o[c-2]>>this.F2:0),l=this.FV/h,p=(1<=0&&(r[r.t++]=1,r.subTo(y,r)),E.ONE.dlShiftTo(c,y),y.subTo(o,o);o.t=0;){var m=r[--g]==f?this.DM:Math.floor(r[g]*l+(r[g-1]+d)*p);if((r[g]+=o.am(0,m,r,v,0,c))0&&r.rShiftTo(u,r),s<0&&E.ZERO.subTo(r,r)}}},E.prototype.invDigit=function vt(){if(this.t<1)return 0;var t=this[0];if(0==(1&t))return 0;var e=3&t;return(e=(e=(e=(e=e*(2-(15&t)*e)&15)*(2-(255&t)*e)&255)*(2-((65535&t)*e&65535))&65535)*(2-t*e%this.DV)%this.DV)>0?this.DV-e:-e},E.prototype.isEven=function yt(){return 0==(this.t>0?1&this[0]:this.s)},E.prototype.exp=function mt(t,e){if(t>4294967295||t<1)return E.ONE;var r=x(),n=x(),i=e.convert(this),o=L(t)-1;for(i.copyTo(r);--o>=0;)if(e.sqrTo(r,n),(t&1<0)e.mulTo(n,i,r);else{var s=r;r=n,n=s}return e.revert(r)},E.prototype.toString=function _t(t){if(this.s<0)return"-"+this.negate().toString(t);var e;if(16==t)e=4;else if(8==t)e=3;else if(2==t)e=1;else if(32==t)e=5;else{if(4!=t)return this.toRadix(t);e=2}var r,n=(1<0)for(a>a)>0&&(i=!0,o=O(r));s>=0;)a>(a+=this.DB-e)):(r=this[s]>>(a-=e)&n,a<=0&&(a+=this.DB,--s)),r>0&&(i=!0),i&&(o+=O(r));return i?o:"0"},E.prototype.negate=function St(){var t=x();return E.ZERO.subTo(this,t),t},E.prototype.abs=function bt(){return this.s<0?this.negate():this},E.prototype.compareTo=function wt(t){var e=this.s-t.s;if(0!=e)return e;var r=this.t;if(0!=(e=r-t.t))return this.s<0?-e:e;for(;--r>=0;)if(0!=(e=this[r]-t[r]))return e;return 0},E.prototype.bitLength=function Ft(){return this.t<=0?0:this.DB*(this.t-1)+L(this[this.t-1]^this.s&this.DM)},E.prototype.mod=function Et(t){var e=x();return this.abs().divRemTo(t,null,e),this.s<0&&e.compareTo(E.ZERO)>0&&t.subTo(e,e),e},E.prototype.modPowInt=function xt(t,e){var r;return r=t<256||e.isEven()?new M(e):new j(e),this.exp(t,r)},E.ZERO=N(0),E.ONE=N(1),W.prototype.convert=J,W.prototype.revert=J,W.prototype.mulTo=function At(t,e,r){t.multiplyTo(e,r)},W.prototype.sqrTo=function kt(t,e){t.squareTo(e)},z.prototype.convert=function Pt(t){if(t.s<0||t.t>2*this.m.t)return t.mod(this.m);if(t.compareTo(this.m)<0)return t;var e=x();return t.copyTo(e),this.reduce(e),e},z.prototype.revert=function Ct(t){return t},z.prototype.reduce=function Tt(t){for(t.drShiftTo(this.m.t-1,this.r2),t.t>this.m.t+1&&(t.t=this.m.t+1,t.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);t.compareTo(this.r2)<0;)t.dAddOffset(1,this.m.t+1);for(t.subTo(this.r2,t);t.compareTo(this.m)>=0;)t.subTo(this.m,t)},z.prototype.mulTo=function Rt(t,e,r){t.multiplyTo(e,r),this.reduce(r)},z.prototype.sqrTo=function It(t,e){t.squareTo(e),this.reduce(e)};var Ot=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],Dt=(1<<26)/Ot[Ot.length-1]; +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +function Nt(){this.i=0,this.j=0,this.S=new Array}E.prototype.chunkSize=function Lt(t){return Math.floor(Math.LN2*this.DB/Math.log(t))},E.prototype.toRadix=function Mt(t){if(null==t&&(t=10),0==this.signum()||t<2||t>36)return"0";var e=this.chunkSize(t),r=Math.pow(t,e),n=N(r),i=x(),o=x(),s="";for(this.divRemTo(n,i,o);i.signum()>0;)s=(r+o.intValue()).toString(t).substr(1)+s,i.divRemTo(n,i,o);return o.intValue().toString(t)+s},E.prototype.fromRadix=function jt(t,e){this.fromInt(0),null==e&&(e=10);for(var r=this.chunkSize(e),n=Math.pow(e,r),i=!1,o=0,s=0,a=0;a=r&&(this.dMultiply(n),this.dAddOffset(s,0),o=0,s=0))}o>0&&(this.dMultiply(Math.pow(e,o)),this.dAddOffset(s,0)),i&&E.ZERO.subTo(this,this)},E.prototype.fromNumber=function Ut(t,e,r){if("number"==typeof e)if(t<2)this.fromInt(1);else for(this.fromNumber(t,r),this.testBit(t-1)||this.bitwiseTo(E.ONE.shiftLeft(t-1),B,this),this.isEven()&&this.dAddOffset(1,0);!this.isProbablePrime(e);)this.dAddOffset(2,0),this.bitLength()>t&&this.subTo(E.ONE.shiftLeft(t-1),this);else{var n=new Array,i=7&t;n.length=1+(t>>3),e.nextBytes(n),i>0?n[0]&=(1<>=this.DB;if(t.t>=this.DB;n+=this.s}else{for(n+=this.s;r>=this.DB;n+=t.s}e.s=n<0?-1:0,n>0?e[r++]=n:n<-1&&(e[r++]=this.DV+n),e.t=r,e.clamp()},E.prototype.dMultiply=function Kt(t){this[this.t]=this.am(0,t-1,this,0,0,this.t),++this.t,this.clamp()},E.prototype.dAddOffset=function qt(t,e){if(0!=t){for(;this.t<=e;)this[this.t++]=0;for(this[e]+=t;this[e]>=this.DV;)this[e]-=this.DV,++e>=this.t&&(this[this.t++]=0),++this[e]}},E.prototype.multiplyLowerTo=function Wt(t,e,r){var n,i=Math.min(this.t+t.t,e);for(r.s=0,r.t=i;i>0;)r[--i]=0;for(n=r.t-this.t;i=0;)r[n]=0;for(n=Math.max(e-this.t,0);n0)if(0==e)r=this[0]%t;else for(var n=this.t-1;n>=0;--n)r=(e*r+this[n])%t;return r},E.prototype.millerRabin=function Yt(t){var e=this.subtract(E.ONE),r=e.getLowestSetBit();if(r<=0)return!1;var n=e.shiftRight(r);(t=t+1>>1)>Ot.length&&(t=Ot.length);for(var i=x(),o=0;o>24},E.prototype.shortValue=function Qt(){return 0==this.t?this.s:this[0]<<16>>16},E.prototype.signum=function Zt(){return this.s<0?-1:this.t<=0||1==this.t&&this[0]<=0?0:1},E.prototype.toByteArray=function te(){var t=this.t,e=new Array;e[0]=this.s;var r,n=this.DB-t*this.DB%8,i=0;if(t-- >0)for(n>n)!=(this.s&this.DM)>>n&&(e[i++]=r|this.s<=0;)n<8?(r=(this[t]&(1<>(n+=this.DB-8)):(r=this[t]>>(n-=8)&255,n<=0&&(n+=this.DB,--t)),0!=(128&r)&&(r|=-256),0==i&&(128&this.s)!=(128&r)&&++i,(i>0||r!=this.s)&&(e[i++]=r);return e},E.prototype.equals=function ee(t){return 0==this.compareTo(t)},E.prototype.min=function re(t){return this.compareTo(t)<0?this:t},E.prototype.max=function ne(t){return this.compareTo(t)>0?this:t},E.prototype.and=function ie(t){var e=x();return this.bitwiseTo(t,U,e),e},E.prototype.or=function oe(t){var e=x();return this.bitwiseTo(t,B,e),e},E.prototype.xor=function se(t){var e=x();return this.bitwiseTo(t,H,e),e},E.prototype.andNot=function ae(t){var e=x();return this.bitwiseTo(t,V,e),e},E.prototype.not=function ue(){for(var t=x(),e=0;e=this.t?0!=this.s:0!=(this[e]&1<1){var f=x();for(n.sqrTo(s[1],f);a<=c;)s[a]=x(),n.mulTo(f,s[a-2],s[a]),a+=2}var h,l,p=t.t-1,d=!0,g=x();for(i=L(t[p])-1;p>=0;){for(i>=u?h=t[p]>>i-u&c:(h=(t[p]&(1<0&&(h|=t[p-1]>>this.DB+i-u)),a=r;0==(1&h);)h>>=1,--a;if((i-=a)<0&&(i+=this.DB,--p),d)s[h].copyTo(o),d=!1;else{for(;a>1;)n.sqrTo(o,g),n.sqrTo(g,o),a-=2;a>0?n.sqrTo(o,g):(l=o,o=g,g=l),n.mulTo(g,s[h],o)}for(;p>=0&&0==(t[p]&1<=0?(r.subTo(n,r),e&&i.subTo(s,i),o.subTo(a,o)):(n.subTo(r,n),e&&s.subTo(i,s),a.subTo(o,a))}return 0!=n.compareTo(E.ONE)?E.ZERO:a.compareTo(t)>=0?a.subtract(t):a.signum()<0?(a.addTo(t,a),a.signum()<0?a.add(t):a):a},E.prototype.pow=function xe(t){return this.exp(t,new W)},E.prototype.gcd=function Ae(t){var e=this.s<0?this.negate():this.clone(),r=t.s<0?t.negate():t.clone();if(e.compareTo(r)<0){var n=e;e=r,r=n}var i=e.getLowestSetBit(),o=r.getLowestSetBit();if(o<0)return e;for(i0&&(e.rShiftTo(o,e),r.rShiftTo(o,r));e.signum()>0;)(i=e.getLowestSetBit())>0&&e.rShiftTo(i,e),(i=r.getLowestSetBit())>0&&r.rShiftTo(i,r),e.compareTo(r)>=0?(e.subTo(r,e),e.rShiftTo(1,e)):(r.subTo(e,r),r.rShiftTo(1,r));return o>0&&r.lShiftTo(o,r),r},E.prototype.isProbablePrime=function ke(t){var e,r=this.abs();if(1==r.t&&r[0]<=Ot[Ot.length-1]){for(e=0;e>8&255,Ie[Oe++]^=e>>16&255,Ie[Oe++]^=e>>24&255,Oe>=De&&(Oe-=De)}((new Date).getTime())}if(null==Ie){var Le;if(Ie=new Array,Oe=0,void 0!==i&&(void 0!==i.crypto||void 0!==i.msCrypto)){var Me=i.crypto||i.msCrypto;if(Me.getRandomValues){var je=new Uint8Array(32);for(Me.getRandomValues(je),Le=0;Le<32;++Le)Ie[Oe++]=je[Le]}else if("Netscape"==n.appName&&n.appVersion<"5"){var Ue=i.crypto.random(32);for(Le=0;Le>>8,Ie[Oe++]=255&Le;Oe=0,Ne()}function Be(){if(null==Re){for(Ne(),(Re=function t(){return new Nt}()).init(Ie),Oe=0;Oe>24,(16711680&i)>>16,(65280&i)>>8,255&i]))),i+=1;return n}function qe(){this.n=null,this.e=0,this.d=null,this.p=null,this.q=null,this.dmp1=null,this.dmq1=null,this.coeff=null} +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +function We(t,e){this.x=e,this.q=t}function Je(t,e,r,n){this.curve=t,this.x=e,this.y=r,this.z=null==n?E.ONE:n,this.zinv=null}function ze(t,e,r){this.q=t,this.a=this.fromBigInteger(e),this.b=this.fromBigInteger(r),this.infinity=new Je(this,null,null)}He.prototype.nextBytes=function Ye(t){var e;for(e=0;e0&&e.length>0))throw"Invalid RSA public key";this.n=Ve(t,16),this.e=parseInt(e,16)}},qe.prototype.encrypt=function $e(t){var e=function r(t,e){if(e=0&&e>0;){var i=t.charCodeAt(n--);i<128?r[--e]=i:i>127&&i<2048?(r[--e]=63&i|128,r[--e]=i>>6|192):(r[--e]=63&i|128,r[--e]=i>>6&63|128,r[--e]=i>>12|224)}r[--e]=0;for(var o=new He,s=new Array;e>2;){for(s[0]=0;0==s[0];)o.nextBytes(s);r[--e]=s[0]}return r[--e]=2,r[--e]=0,new E(r)}(t,this.n.bitLength()+7>>3);if(null==e)return null;var n=this.doPublic(e);if(null==n)return null;var i=n.toString(16);return 0==(1&i.length)?i:"0"+i},qe.prototype.encryptOAEP=function Qe(t,e,r){var n=function i(t,e,r,n){var i=Er.crypto.MessageDigest,o=Er.crypto.Util,s=null;if(r||(r="sha1"),"string"==typeof r&&(s=i.getCanonicalAlgName(r),n=i.getHashLength(s),r=function t(e){return jr(o.hashHex(Ur(e),s))}),t.length+2*n+2>e)throw"Message too long for RSA";var a,u="";for(a=0;a>3,e,r);if(null==n)return null;var o=this.doPublic(n);if(null==o)return null;var s=o.toString(16);return 0==(1&s.length)?s:"0"+s},qe.prototype.type="RSA",We.prototype.equals=function Ze(t){return t==this||this.q.equals(t.q)&&this.x.equals(t.x)},We.prototype.toBigInteger=function tr(){return this.x},We.prototype.negate=function er(){return new We(this.q,this.x.negate().mod(this.q))},We.prototype.add=function rr(t){return new We(this.q,this.x.add(t.toBigInteger()).mod(this.q))},We.prototype.subtract=function nr(t){return new We(this.q,this.x.subtract(t.toBigInteger()).mod(this.q))},We.prototype.multiply=function ir(t){return new We(this.q,this.x.multiply(t.toBigInteger()).mod(this.q))},We.prototype.square=function or(){return new We(this.q,this.x.square().mod(this.q))},We.prototype.divide=function sr(t){return new We(this.q,this.x.multiply(t.toBigInteger().modInverse(this.q)).mod(this.q))},Je.prototype.getX=function ar(){return null==this.zinv&&(this.zinv=this.z.modInverse(this.curve.q)),this.curve.fromBigInteger(this.x.toBigInteger().multiply(this.zinv).mod(this.curve.q))},Je.prototype.getY=function ur(){return null==this.zinv&&(this.zinv=this.z.modInverse(this.curve.q)),this.curve.fromBigInteger(this.y.toBigInteger().multiply(this.zinv).mod(this.curve.q))},Je.prototype.equals=function cr(t){return t==this||(this.isInfinity()?t.isInfinity():t.isInfinity()?this.isInfinity():!!t.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(t.z)).mod(this.curve.q).equals(E.ZERO)&&t.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(t.z)).mod(this.curve.q).equals(E.ZERO))},Je.prototype.isInfinity=function fr(){return null==this.x&&null==this.y||this.z.equals(E.ZERO)&&!this.y.toBigInteger().equals(E.ZERO)},Je.prototype.negate=function hr(){return new Je(this.curve,this.x,this.y.negate(),this.z)},Je.prototype.add=function lr(t){if(this.isInfinity())return t;if(t.isInfinity())return this;var e=t.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(t.z)).mod(this.curve.q),r=t.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(t.z)).mod(this.curve.q);if(E.ZERO.equals(r))return E.ZERO.equals(e)?this.twice():this.curve.getInfinity();var n=new E("3"),i=this.x.toBigInteger(),o=this.y.toBigInteger(),s=(t.x.toBigInteger(),t.y.toBigInteger(),r.square()),a=s.multiply(r),u=i.multiply(s),c=e.square().multiply(this.z),f=c.subtract(u.shiftLeft(1)).multiply(t.z).subtract(a).multiply(r).mod(this.curve.q),h=u.multiply(n).multiply(e).subtract(o.multiply(a)).subtract(c.multiply(e)).multiply(t.z).add(e.multiply(a)).mod(this.curve.q),l=a.multiply(this.z).multiply(t.z).mod(this.curve.q);return new Je(this.curve,this.curve.fromBigInteger(f),this.curve.fromBigInteger(h),l)},Je.prototype.twice=function pr(){if(this.isInfinity())return this;if(0==this.y.toBigInteger().signum())return this.curve.getInfinity();var t=new E("3"),e=this.x.toBigInteger(),r=this.y.toBigInteger(),n=r.multiply(this.z),i=n.multiply(r).mod(this.curve.q),o=this.curve.a.toBigInteger(),s=e.square().multiply(t);E.ZERO.equals(o)||(s=s.add(this.z.square().multiply(o)));var a=(s=s.mod(this.curve.q)).square().subtract(e.shiftLeft(3).multiply(i)).shiftLeft(1).multiply(n).mod(this.curve.q),u=s.multiply(t).multiply(e).subtract(i.shiftLeft(1)).shiftLeft(2).multiply(i).subtract(s.square().multiply(s)).mod(this.curve.q),c=n.square().multiply(n).shiftLeft(3).mod(this.curve.q);return new Je(this.curve,this.curve.fromBigInteger(a),this.curve.fromBigInteger(u),c)},Je.prototype.multiply=function dr(t){if(this.isInfinity())return this;if(0==t.signum())return this.curve.getInfinity();var e,r=t,n=r.multiply(new E("3")),i=this.negate(),o=this;for(e=n.bitLength()-2;e>0;--e){o=o.twice();var s=n.testBit(e);s!=r.testBit(e)&&(o=o.add(s?this:i))}return o},Je.prototype.multiplyTwo=function gr(t,e,r){var n;n=t.bitLength()>r.bitLength()?t.bitLength()-1:r.bitLength()-1;for(var i=this.curve.getInfinity(),o=this.add(e);n>=0;)i=i.twice(),t.testBit(n)?i=r.testBit(n)?i.add(o):i.add(this):r.testBit(n)&&(i=i.add(e)),--n;return i},ze.prototype.getQ=function vr(){return this.q},ze.prototype.getA=function yr(){return this.a},ze.prototype.getB=function mr(){return this.b},ze.prototype.equals=function _r(t){return t==this||this.q.equals(t.q)&&this.a.equals(t.a)&&this.b.equals(t.b)},ze.prototype.getInfinity=function Sr(){return this.infinity},ze.prototype.fromBigInteger=function br(t){return new We(this.q,t)},ze.prototype.decodePointHex=function wr(t){switch(parseInt(t.substr(0,2),16)){case 0:return this.infinity;case 2:case 3:return null;case 4:case 6:case 7:var e=(t.length-2)/2,r=t.substr(2,e),n=t.substr(e+2,e);return new Je(this,this.fromBigInteger(new E(r,16)),this.fromBigInteger(new E(n,16)));default:return null}}, +/*! (c) Stefan Thomas | https://github.com/bitcoinjs/bitcoinjs-lib + */ +We.prototype.getByteLength=function(){return Math.floor((this.toBigInteger().bitLength()+7)/8)},Je.prototype.getEncoded=function(t){var e=function t(e,r){var n=e.toByteArrayUnsigned();if(rn.length;)n.unshift(0);return n},r=this.getX().toBigInteger(),n=this.getY().toBigInteger(),i=e(r,32);return t?n.isEven()?i.unshift(2):i.unshift(3):(i.unshift(4),i=i.concat(e(n,32))),i},Je.decodeFrom=function(t,e){e[0];var r=e.length-1,n=e.slice(1,1+r/2),i=e.slice(1+r/2,1+r);n.unshift(0),i.unshift(0);var o=new E(n),s=new E(i);return new Je(t,t.fromBigInteger(o),t.fromBigInteger(s))},Je.decodeFromHex=function(t,e){e.substr(0,2);var r=e.length-2,n=e.substr(2,r/2),i=e.substr(2+r/2,r/2),o=new E(n,16),s=new E(i,16);return new Je(t,t.fromBigInteger(o),t.fromBigInteger(s))},Je.prototype.add2D=function(t){if(this.isInfinity())return t;if(t.isInfinity())return this;if(this.x.equals(t.x))return this.y.equals(t.y)?this.twice():this.curve.getInfinity();var e=t.x.subtract(this.x),r=t.y.subtract(this.y).divide(e),n=r.square().subtract(this.x).subtract(t.x),i=r.multiply(this.x.subtract(n)).subtract(this.y);return new Je(this.curve,n,i)},Je.prototype.twice2D=function(){if(this.isInfinity())return this;if(0==this.y.toBigInteger().signum())return this.curve.getInfinity();var t=this.curve.fromBigInteger(E.valueOf(2)),e=this.curve.fromBigInteger(E.valueOf(3)),r=this.x.square().multiply(e).add(this.curve.a).divide(this.y.multiply(t)),n=r.square().subtract(this.x.multiply(t)),i=r.multiply(this.x.subtract(n)).subtract(this.y);return new Je(this.curve,n,i)},Je.prototype.multiply2D=function(t){if(this.isInfinity())return this;if(0==t.signum())return this.curve.getInfinity();var e,r=t,n=r.multiply(new E("3")),i=this.negate(),o=this;for(e=n.bitLength()-2;e>0;--e){o=o.twice();var s=n.testBit(e);s!=r.testBit(e)&&(o=o.add2D(s?this:i))}return o},Je.prototype.isOnCurve=function(){var t=this.getX().toBigInteger(),e=this.getY().toBigInteger(),r=this.curve.getA().toBigInteger(),n=this.curve.getB().toBigInteger(),i=this.curve.getQ(),o=e.multiply(e).mod(i),s=t.multiply(t).multiply(t).add(r.multiply(t)).add(n).mod(i);return o.equals(s)},Je.prototype.toString=function(){return"("+this.getX().toBigInteger().toString()+","+this.getY().toBigInteger().toString()+")"},Je.prototype.validate=function(){var t=this.curve.getQ();if(this.isInfinity())throw new Error("Point is at infinity.");var e=this.getX().toBigInteger(),r=this.getY().toBigInteger();if(e.compareTo(E.ONE)<0||e.compareTo(t.subtract(E.ONE))>0)throw new Error("x coordinate out of bounds");if(r.compareTo(E.ONE)<0||r.compareTo(t.subtract(E.ONE))>0)throw new Error("y coordinate out of bounds");if(!this.isOnCurve())throw new Error("Point is not on the curve.");if(this.multiply(t).isInfinity())throw new Error("Point is not a scalar multiple of G.");return!0}; +/*! Mike Samuel (c) 2009 | code.google.com/p/json-sans-eval + */ +var Fr=function(){var t=new RegExp('(?:false|true|null|[\\{\\}\\[\\]]|(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)|(?:"(?:[^\\0-\\x08\\x0a-\\x1f"\\\\]|\\\\(?:["/\\\\bfnrt]|u[0-9A-Fa-f]{4}))*"))',"g"),e=new RegExp("\\\\(?:([^u])|u(.{4}))","g"),n={'"':'"',"/":"/","\\":"\\",b:"\b",f:"\f",n:"\n",r:"\r",t:"\t"};function i(t,e,r){return e?n[e]:String.fromCharCode(parseInt(r,16))}var o=new String(""),s=(Object,Array,Object.hasOwnProperty);return function(n,a){var u,c,f=n.match(t),h=f[0],l=!1;"{"===h?u={}:"["===h?u=[]:(u=[],l=!0);for(var p=[u],d=1-l,g=f.length;d=0;)delete i[o[f]]}return a.call(e,n,i)}({"":u},"")}return u}}();void 0!==Er&&Er||(e.KJUR=Er={}),void 0!==Er.asn1&&Er.asn1||(Er.asn1={}),Er.asn1.ASN1Util=new function(){this.integerToByteHex=function(t){var e=t.toString(16);return e.length%2==1&&(e="0"+e),e},this.bigIntToMinTwosComplementsHex=function(t){var e=t.toString(16);if("-"!=e.substr(0,1))e.length%2==1?e="0"+e:e.match(/^[0-7]/)||(e="00"+e);else{var r=e.substr(1).length;r%2==1?r+=1:e.match(/^[0-7]/)||(r+=2);for(var n="",i=0;i15)throw"ASN.1 length too long to represent by 8x: n = "+t.toString(16);return(128+r).toString(16)+e},this.getEncodedHex=function(){return(null==this.hTLV||this.isModified)&&(this.hV=this.getFreshValueHex(),this.hL=this.getLengthHexFromValue(),this.hTLV=this.hT+this.hL+this.hV,this.isModified=!1),this.hTLV},this.getValueHex=function(){return this.getEncodedHex(),this.hV},this.getFreshValueHex=function(){return""}},Er.asn1.DERAbstractString=function(t){Er.asn1.DERAbstractString.superclass.constructor.call(this);this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=Lr(this.s).toLowerCase()},this.setStringHex=function(t){this.hTLV=null,this.isModified=!0,this.s=null,this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&("string"==typeof t?this.setString(t):void 0!==t.str?this.setString(t.str):void 0!==t.hex&&this.setStringHex(t.hex))},o.lang.extend(Er.asn1.DERAbstractString,Er.asn1.ASN1Object),Er.asn1.DERAbstractTime=function(t){Er.asn1.DERAbstractTime.superclass.constructor.call(this);this.localDateToUTC=function(t){return utc=t.getTime()+6e4*t.getTimezoneOffset(),new Date(utc)},this.formatDate=function(t,e,r){var n=this.zeroPadding,i=this.localDateToUTC(t),o=String(i.getFullYear());"utc"==e&&(o=o.substr(2,2));var s=o+n(String(i.getMonth()+1),2)+n(String(i.getDate()),2)+n(String(i.getHours()),2)+n(String(i.getMinutes()),2)+n(String(i.getSeconds()),2);if(!0===r){var a=i.getMilliseconds();if(0!=a){var u=n(String(a),3);s=s+"."+(u=u.replace(/[0]+$/,""))}}return s+"Z"},this.zeroPadding=function(t,e){return t.length>=e?t:new Array(e-t.length+1).join("0")+t},this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=Rr(t)},this.setByDateValue=function(t,e,r,n,i,o){var s=new Date(Date.UTC(t,e-1,r,n,i,o,0));this.setByDate(s)},this.getFreshValueHex=function(){return this.hV}},o.lang.extend(Er.asn1.DERAbstractTime,Er.asn1.ASN1Object),Er.asn1.DERAbstractStructured=function(t){Er.asn1.DERAbstractString.superclass.constructor.call(this);this.setByASN1ObjectArray=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array=t},this.appendASN1Object=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array.push(t)},this.asn1Array=new Array,void 0!==t&&void 0!==t.array&&(this.asn1Array=t.array)},o.lang.extend(Er.asn1.DERAbstractStructured,Er.asn1.ASN1Object),Er.asn1.DERBoolean=function(){Er.asn1.DERBoolean.superclass.constructor.call(this),this.hT="01",this.hTLV="0101ff"},o.lang.extend(Er.asn1.DERBoolean,Er.asn1.ASN1Object),Er.asn1.DERInteger=function(t){Er.asn1.DERInteger.superclass.constructor.call(this),this.hT="02",this.setByBigInteger=function(t){this.hTLV=null,this.isModified=!0,this.hV=Er.asn1.ASN1Util.bigIntToMinTwosComplementsHex(t)},this.setByInteger=function(t){var e=new E(String(t),10);this.setByBigInteger(e)},this.setValueHex=function(t){this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&(void 0!==t.bigint?this.setByBigInteger(t.bigint):void 0!==t.int?this.setByInteger(t.int):"number"==typeof t?this.setByInteger(t):void 0!==t.hex&&this.setValueHex(t.hex))},o.lang.extend(Er.asn1.DERInteger,Er.asn1.ASN1Object),Er.asn1.DERBitString=function(t){if(void 0!==t&&void 0!==t.obj){var e=Er.asn1.ASN1Util.newObject(t.obj);t.hex="00"+e.getEncodedHex()}Er.asn1.DERBitString.superclass.constructor.call(this),this.hT="03",this.setHexValueIncludingUnusedBits=function(t){this.hTLV=null,this.isModified=!0,this.hV=t},this.setUnusedBitsAndHexValue=function(t,e){if(t<0||7i.length&&(i=n[r]);return(t=t.replace(i,"::")).slice(1,-1)}function $r(t){var e="malformed hex value";if(!t.match(/^([0-9A-Fa-f][0-9A-Fa-f]){1,}$/))throw e;if(8!=t.length)return 32==t.length?Xr(t):t;try{return parseInt(t.substr(0,2),16)+"."+parseInt(t.substr(2,2),16)+"."+parseInt(t.substr(4,2),16)+"."+parseInt(t.substr(6,2),16)}catch(t){throw e}}function Qr(t){for(var e=encodeURIComponent(t),r="",n=0;n"7"?"00"+t:t}kr.getLblen=function(t,e){if("8"!=t.substr(e+2,1))return 1;var r=parseInt(t.substr(e+3,1));return 0==r?-1:0=2*o)break;if(a>=200)break;n.push(u),s=u,a++}return n},kr.getNthChildIdx=function(t,e,r){return kr.getChildIdx(t,e)[r]},kr.getIdxbyList=function(t,e,r,n){var i,o,s=kr;if(0==r.length){if(void 0!==n&&t.substr(e,2)!==n)throw"checking tag doesn't match: "+t.substr(e,2)+"!="+n;return e}return i=r.shift(),o=s.getChildIdx(t,e),s.getIdxbyList(t,o[i],r,n)},kr.getTLVbyList=function(t,e,r,n){var i=kr,o=i.getIdxbyList(t,e,r);if(void 0===o)throw"can't find nthList object";if(void 0!==n&&t.substr(o,2)!=n)throw"checking tag doesn't match: "+t.substr(o,2)+"!="+n;return i.getTLV(t,o)},kr.getVbyList=function(t,e,r,n,i){var o,s,a=kr;if(void 0===(o=a.getIdxbyList(t,e,r,n)))throw"can't find nthList object";return s=a.getV(t,o),!0===i&&(s=s.substr(2)),s},kr.hextooidstr=function(t){var e=function t(e,r){return e.length>=r?e:new Array(r-e.length+1).join("0")+e},r=[],n=t.substr(0,2),i=parseInt(n,16);r[0]=new String(Math.floor(i/40)),r[1]=new String(i%40);for(var o=t.substr(2),s=[],a=0;a0&&(f=f+"."+u.join(".")),f},kr.dump=function(t,e,r,n){var i=kr,o=i.getV,s=i.dump,a=i.getChildIdx,u=t;t instanceof Er.asn1.ASN1Object&&(u=t.getEncodedHex());var c=function t(e,r){return e.length<=2*r?e:e.substr(0,r)+"..(total "+e.length/2+"bytes).."+e.substr(e.length-r,r)};void 0===e&&(e={ommit_long_octet:32}),void 0===r&&(r=0),void 0===n&&(n="");var f=e.ommit_long_octet;if("01"==u.substr(r,2))return"00"==(h=o(u,r))?n+"BOOLEAN FALSE\n":n+"BOOLEAN TRUE\n";if("02"==u.substr(r,2))return n+"INTEGER "+c(h=o(u,r),f)+"\n";if("03"==u.substr(r,2))return n+"BITSTRING "+c(h=o(u,r),f)+"\n";if("04"==u.substr(r,2)){var h=o(u,r);if(i.isASN1HEX(h)){var l=n+"OCTETSTRING, encapsulates\n";return l+=s(h,e,0,n+" ")}return n+"OCTETSTRING "+c(h,f)+"\n"}if("05"==u.substr(r,2))return n+"NULL\n";if("06"==u.substr(r,2)){var p=o(u,r),d=Er.asn1.ASN1Util.oidHexToInt(p),g=Er.asn1.x509.OID.oid2name(d),v=d.replace(/\./g," ");return""!=g?n+"ObjectIdentifier "+g+" ("+v+")\n":n+"ObjectIdentifier ("+v+")\n"}if("0c"==u.substr(r,2))return n+"UTF8String '"+Mr(o(u,r))+"'\n";if("13"==u.substr(r,2))return n+"PrintableString '"+Mr(o(u,r))+"'\n";if("14"==u.substr(r,2))return n+"TeletexString '"+Mr(o(u,r))+"'\n";if("16"==u.substr(r,2))return n+"IA5String '"+Mr(o(u,r))+"'\n";if("17"==u.substr(r,2))return n+"UTCTime "+Mr(o(u,r))+"\n";if("18"==u.substr(r,2))return n+"GeneralizedTime "+Mr(o(u,r))+"\n";if("30"==u.substr(r,2)){if("3000"==u.substr(r,4))return n+"SEQUENCE {}\n";l=n+"SEQUENCE\n";var y=e;if((2==(S=a(u,r)).length||3==S.length)&&"06"==u.substr(S[0],2)&&"04"==u.substr(S[S.length-1],2)){g=i.oidname(o(u,S[0]));var m=JSON.parse(JSON.stringify(e));m.x509ExtName=g,y=m}for(var _=0;_i)throw"key is too short for SigAlg: keylen="+r+","+e;for(var o="0001",s="00"+n,a="",u=i-o.length-s.length,c=0;c=0)return!1;if(r.compareTo(E.ONE)<0||r.compareTo(i)>=0)return!1;var s=r.modInverse(i),a=t.multiply(s).mod(i),u=e.multiply(s).mod(i);return o.multiply(a).add(n.multiply(u)).getX().toBigInteger().mod(i).equals(e)},this.serializeSig=function(t,e){var r=t.toByteArraySigned(),n=e.toByteArraySigned(),i=[];return i.push(2),i.push(r.length),(i=i.concat(r)).push(2),i.push(n.length),(i=i.concat(n)).unshift(i.length),i.unshift(48),i},this.parseSig=function(t){var e;if(48!=t[0])throw new Error("Signature not a valid DERSequence");if(2!=t[e=2])throw new Error("First element in signature must be a DERInteger");var r=t.slice(e+2,e+2+t[e+1]);if(2!=t[e+=2+t[e+1]])throw new Error("Second element in signature must be a DERInteger");var n=t.slice(e+2,e+2+t[e+1]);return e+=2+t[e+1],{r:E.fromByteArrayUnsigned(r),s:E.fromByteArrayUnsigned(n)}},this.parseSigCompact=function(t){if(65!==t.length)throw"Signature has the wrong length";var e=t[0]-27;if(e<0||e>7)throw"Invalid signature type";var r=this.ecparams.n;return{r:E.fromByteArrayUnsigned(t.slice(1,33)).mod(r),s:E.fromByteArrayUnsigned(t.slice(33,65)).mod(r),i:e}},this.readPKCS5PrvKeyHex=function(t){var e,r,n,i=kr,o=Er.crypto.ECDSA.getName,s=i.getVbyList;if(!1===i.isASN1HEX(t))throw"not ASN.1 hex string";try{e=s(t,0,[2,0],"06"),r=s(t,0,[1],"04");try{n=s(t,0,[3,0],"03").substr(2)}catch(t){}}catch(t){throw"malformed PKCS#1/5 plain ECC private key"}if(this.curveName=o(e),void 0===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(n),this.setPrivateKeyHex(r),this.isPublic=!1},this.readPKCS8PrvKeyHex=function(t){var e,r,n,i=kr,o=Er.crypto.ECDSA.getName,s=i.getVbyList;if(!1===i.isASN1HEX(t))throw"not ASN.1 hex string";try{s(t,0,[1,0],"06"),e=s(t,0,[1,1],"06"),r=s(t,0,[2,0,1],"04");try{n=s(t,0,[2,0,2,0],"03").substr(2)}catch(t){}}catch(t){throw"malformed PKCS#8 plain ECC private key"}if(this.curveName=o(e),void 0===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(n),this.setPrivateKeyHex(r),this.isPublic=!1},this.readPKCS8PubKeyHex=function(t){var e,r,n=kr,i=Er.crypto.ECDSA.getName,o=n.getVbyList;if(!1===n.isASN1HEX(t))throw"not ASN.1 hex string";try{o(t,0,[0,0],"06"),e=o(t,0,[0,1],"06"),r=o(t,0,[1],"03").substr(2)}catch(t){throw"malformed PKCS#8 ECC public key"}if(this.curveName=i(e),null===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(r)},this.readCertPubKeyHex=function(t,e){5!==e&&(e=6);var r,n,i=kr,o=Er.crypto.ECDSA.getName,s=i.getVbyList;if(!1===i.isASN1HEX(t))throw"not ASN.1 hex string";try{r=s(t,0,[0,e,0,1],"06"),n=s(t,0,[0,e,1],"03").substr(2)}catch(t){throw"malformed X.509 certificate ECC public key"}if(this.curveName=o(r),null===this.curveName)throw"unsupported curve name";this.setNamedCurve(this.curveName),this.setPublicKeyHex(n)},void 0!==t&&void 0!==t.curve&&(this.curveName=t.curve),void 0===this.curveName&&(this.curveName="secp256r1"),this.setNamedCurve(this.curveName),void 0!==t&&(void 0!==t.prv&&this.setPrivateKeyHex(t.prv),void 0!==t.pub&&this.setPublicKeyHex(t.pub))},Er.crypto.ECDSA.parseSigHex=function(t){var e=Er.crypto.ECDSA.parseSigHexInHexRS(t);return{r:new E(e.r,16),s:new E(e.s,16)}},Er.crypto.ECDSA.parseSigHexInHexRS=function(t){var e=kr,r=e.getChildIdx,n=e.getV;if("30"!=t.substr(0,2))throw"signature is not a ASN.1 sequence";var i=r(t,0);if(2!=i.length)throw"number of signature ASN.1 sequence elements seem wrong";var o=i[0],s=i[1];if("02"!=t.substr(o,2))throw"1st item of sequene of signature is not ASN.1 integer";if("02"!=t.substr(s,2))throw"2nd item of sequene of signature is not ASN.1 integer";return{r:n(t,o),s:n(t,s)}},Er.crypto.ECDSA.asn1SigToConcatSig=function(t){var e=Er.crypto.ECDSA.parseSigHexInHexRS(t),r=e.r,n=e.s;if("00"==r.substr(0,2)&&r.length%32==2&&(r=r.substr(2)),"00"==n.substr(0,2)&&n.length%32==2&&(n=n.substr(2)),r.length%32==30&&(r="00"+r),n.length%32==30&&(n="00"+n),r.length%32!=0)throw"unknown ECDSA sig r length error";if(n.length%32!=0)throw"unknown ECDSA sig s length error";return r+n},Er.crypto.ECDSA.concatSigToASN1Sig=function(t){if(t.length/2*8%128!=0)throw"unknown ECDSA concatinated r-s sig length error";var e=t.substr(0,t.length/2),r=t.substr(t.length/2);return Er.crypto.ECDSA.hexRSSigToASN1Sig(e,r)},Er.crypto.ECDSA.hexRSSigToASN1Sig=function(t,e){var r=new E(t,16),n=new E(e,16);return Er.crypto.ECDSA.biRSSigToASN1Sig(r,n)},Er.crypto.ECDSA.biRSSigToASN1Sig=function(t,e){var r=Er.asn1,n=new r.DERInteger({bigint:t}),i=new r.DERInteger({bigint:e});return new r.DERSequence({array:[n,i]}).getEncodedHex()},Er.crypto.ECDSA.getName=function(t){return"2a8648ce3d030107"===t?"secp256r1":"2b8104000a"===t?"secp256k1":"2b81040022"===t?"secp384r1":-1!=="|secp256r1|NIST P-256|P-256|prime256v1|".indexOf(t)?"secp256r1":-1!=="|secp256k1|".indexOf(t)?"secp256k1":-1!=="|secp384r1|NIST P-384|P-384|".indexOf(t)?"secp384r1":null},void 0!==Er&&Er||(e.KJUR=Er={}),void 0!==Er.crypto&&Er.crypto||(Er.crypto={}),Er.crypto.ECParameterDB=new function(){var t={},e={};function r(t){return new E(t,16)}this.getByName=function(r){var n=r;if(void 0!==e[n]&&(n=e[r]),void 0!==t[n])return t[n];throw"unregistered EC curve name: "+n},this.regist=function(n,i,o,s,a,u,c,f,h,l,p,d){t[n]={};var g=r(o),v=r(s),y=r(a),m=r(u),_=r(c),S=new ze(g,v,y),b=S.decodePointHex("04"+f+h);t[n].name=n,t[n].keylen=i,t[n].curve=S,t[n].G=b,t[n].n=m,t[n].h=_,t[n].oid=p,t[n].info=d;for(var w=0;w=2*u)break}var l={};return l.keyhex=c.substr(0,2*i[e].keylen),l.ivhex=c.substr(2*i[e].keylen,2*i[e].ivlen),l},a=function t(e,r,n,o){var s=y.enc.Base64.parse(e),a=y.enc.Hex.stringify(s);return(0,i[r].proc)(a,n,o)};return{version:"1.0.0",parsePKCS5PEM:function t(e){return o(e)},getKeyAndUnusedIvByPasscodeAndIvsalt:function t(e,r,n){return s(e,r,n)},decryptKeyB64:function t(e,r,n,i){return a(e,r,n,i)},getDecryptedKeyHex:function t(e,r){var n=o(e),i=(n.type,n.cipher),u=n.ivsalt,c=n.data,f=s(i,r,u).keyhex;return a(c,i,f,u)},getEncryptedPKCS5PEMFromPrvKeyHex:function t(e,r,n,o,a){var u="";if(void 0!==o&&null!=o||(o="AES-256-CBC"),void 0===i[o])throw"KEYUTIL unsupported algorithm: "+o;void 0!==a&&null!=a||(a=function t(e){var r=y.lib.WordArray.random(e);return y.enc.Hex.stringify(r)}(i[o].ivlen).toUpperCase());var c=function t(e,r,n,o){return(0,i[r].eproc)(e,n,o)}(r,o,s(o,n,a).keyhex,a);u="-----BEGIN "+e+" PRIVATE KEY-----\r\n";return u+="Proc-Type: 4,ENCRYPTED\r\n",u+="DEK-Info: "+o+","+a+"\r\n",u+="\r\n",u+=c.replace(/(.{64})/g,"$1\r\n"),u+="\r\n-----END "+e+" PRIVATE KEY-----\r\n"},parseHexOfEncryptedPKCS8:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={},s=n(e,0);if(2!=s.length)throw"malformed format: SEQUENCE(0).items != 2: "+s.length;o.ciphertext=i(e,s[1]);var a=n(e,s[0]);if(2!=a.length)throw"malformed format: SEQUENCE(0.0).items != 2: "+a.length;if("2a864886f70d01050d"!=i(e,a[0]))throw"this only supports pkcs5PBES2";var u=n(e,a[1]);if(2!=a.length)throw"malformed format: SEQUENCE(0.0.1).items != 2: "+u.length;var c=n(e,u[1]);if(2!=c.length)throw"malformed format: SEQUENCE(0.0.1.1).items != 2: "+c.length;if("2a864886f70d0307"!=i(e,c[0]))throw"this only supports TripleDES";o.encryptionSchemeAlg="TripleDES",o.encryptionSchemeIV=i(e,c[1]);var f=n(e,u[0]);if(2!=f.length)throw"malformed format: SEQUENCE(0.0.1.0).items != 2: "+f.length;if("2a864886f70d01050c"!=i(e,f[0]))throw"this only supports pkcs5PBKDF2";var h=n(e,f[1]);if(h.length<2)throw"malformed format: SEQUENCE(0.0.1.0.1).items < 2: "+h.length;o.pbkdf2Salt=i(e,h[0]);var l=i(e,h[1]);try{o.pbkdf2Iter=parseInt(l,16)}catch(t){throw"malformed format pbkdf2Iter: "+l}return o},getPBKDF2KeyHexFromParam:function t(e,r){var n=y.enc.Hex.parse(e.pbkdf2Salt),i=e.pbkdf2Iter,o=y.PBKDF2(r,n,{keySize:6,iterations:i});return y.enc.Hex.stringify(o)},_getPlainPKCS8HexFromEncryptedPKCS8PEM:function t(e,r){var n=qr(e,"ENCRYPTED PRIVATE KEY"),i=this.parseHexOfEncryptedPKCS8(n),o=tn.getPBKDF2KeyHexFromParam(i,r),s={};s.ciphertext=y.enc.Hex.parse(i.ciphertext);var a=y.enc.Hex.parse(o),u=y.enc.Hex.parse(i.encryptionSchemeIV),c=y.TripleDES.decrypt(s,a,{iv:u});return y.enc.Hex.stringify(c)},getKeyFromEncryptedPKCS8PEM:function t(e,r){var n=this._getPlainPKCS8HexFromEncryptedPKCS8PEM(e,r);return this.getKeyFromPlainPrivatePKCS8Hex(n)},parsePlainPrivatePKCS8Hex:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={algparam:null};if("30"!=e.substr(0,2))throw"malformed plain PKCS8 private key(code:001)";var s=n(e,0);if(3!=s.length)throw"malformed plain PKCS8 private key(code:002)";if("30"!=e.substr(s[1],2))throw"malformed PKCS8 private key(code:003)";var a=n(e,s[1]);if(2!=a.length)throw"malformed PKCS8 private key(code:004)";if("06"!=e.substr(a[0],2))throw"malformed PKCS8 private key(code:005)";if(o.algoid=i(e,a[0]),"06"==e.substr(a[1],2)&&(o.algparam=i(e,a[1])),"04"!=e.substr(s[2],2))throw"malformed PKCS8 private key(code:006)";return o.keyidx=r.getVidx(e,s[2]),o},getKeyFromPlainPrivatePKCS8PEM:function t(e){var r=qr(e,"PRIVATE KEY");return this.getKeyFromPlainPrivatePKCS8Hex(r)},getKeyFromPlainPrivatePKCS8Hex:function t(e){var r,n=this.parsePlainPrivatePKCS8Hex(e);if("2a864886f70d010101"==n.algoid)r=new qe;else if("2a8648ce380401"==n.algoid)r=new Er.crypto.DSA;else{if("2a8648ce3d0201"!=n.algoid)throw"unsupported private key algorithm";r=new Er.crypto.ECDSA}return r.readPKCS8PrvKeyHex(e),r},_getKeyFromPublicPKCS8Hex:function t(e){var r,n=kr.getVbyList(e,0,[0,0],"06");if("2a864886f70d010101"===n)r=new qe;else if("2a8648ce380401"===n)r=new Er.crypto.DSA;else{if("2a8648ce3d0201"!==n)throw"unsupported PKCS#8 public key hex";r=new Er.crypto.ECDSA}return r.readPKCS8PubKeyHex(e),r},parsePublicRawRSAKeyHex:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={};if("30"!=e.substr(0,2))throw"malformed RSA key(code:001)";var s=n(e,0);if(2!=s.length)throw"malformed RSA key(code:002)";if("02"!=e.substr(s[0],2))throw"malformed RSA key(code:003)";if(o.n=i(e,s[0]),"02"!=e.substr(s[1],2))throw"malformed RSA key(code:004)";return o.e=i(e,s[1]),o},parsePublicPKCS8Hex:function t(e){var r=kr,n=r.getChildIdx,i=r.getV,o={algparam:null},s=n(e,0);if(2!=s.length)throw"outer DERSequence shall have 2 elements: "+s.length;var a=s[0];if("30"!=e.substr(a,2))throw"malformed PKCS8 public key(code:001)";var u=n(e,a);if(2!=u.length)throw"malformed PKCS8 public key(code:002)";if("06"!=e.substr(u[0],2))throw"malformed PKCS8 public key(code:003)";if(o.algoid=i(e,u[0]),"06"==e.substr(u[1],2)?o.algparam=i(e,u[1]):"30"==e.substr(u[1],2)&&(o.algparam={},o.algparam.p=r.getVbyList(e,u[1],[0],"02"),o.algparam.q=r.getVbyList(e,u[1],[1],"02"),o.algparam.g=r.getVbyList(e,u[1],[2],"02")),"03"!=e.substr(s[1],2))throw"malformed PKCS8 public key(code:004)";return o.key=i(e,s[1]).substr(2),o}}}();tn.getKey=function(t,e,r){var n=(v=kr).getChildIdx,i=(v.getV,v.getVbyList),o=Er.crypto,s=o.ECDSA,a=o.DSA,u=qe,c=qr,f=tn;if(void 0!==u&&t instanceof u)return t;if(void 0!==s&&t instanceof s)return t;if(void 0!==a&&t instanceof a)return t;if(void 0!==t.curve&&void 0!==t.xy&&void 0===t.d)return new s({pub:t.xy,curve:t.curve});if(void 0!==t.curve&&void 0!==t.d)return new s({prv:t.d,curve:t.curve});if(void 0===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0===t.d)return(P=new u).setPublic(t.n,t.e),P;if(void 0===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d&&void 0!==t.p&&void 0!==t.q&&void 0!==t.dp&&void 0!==t.dq&&void 0!==t.co&&void 0===t.qi)return(P=new u).setPrivateEx(t.n,t.e,t.d,t.p,t.q,t.dp,t.dq,t.co),P;if(void 0===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d&&void 0===t.p)return(P=new u).setPrivate(t.n,t.e,t.d),P;if(void 0!==t.p&&void 0!==t.q&&void 0!==t.g&&void 0!==t.y&&void 0===t.x)return(P=new a).setPublic(t.p,t.q,t.g,t.y),P;if(void 0!==t.p&&void 0!==t.q&&void 0!==t.g&&void 0!==t.y&&void 0!==t.x)return(P=new a).setPrivate(t.p,t.q,t.g,t.y,t.x),P;if("RSA"===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0===t.d)return(P=new u).setPublic(Nr(t.n),Nr(t.e)),P;if("RSA"===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d&&void 0!==t.p&&void 0!==t.q&&void 0!==t.dp&&void 0!==t.dq&&void 0!==t.qi)return(P=new u).setPrivateEx(Nr(t.n),Nr(t.e),Nr(t.d),Nr(t.p),Nr(t.q),Nr(t.dp),Nr(t.dq),Nr(t.qi)),P;if("RSA"===t.kty&&void 0!==t.n&&void 0!==t.e&&void 0!==t.d)return(P=new u).setPrivate(Nr(t.n),Nr(t.e),Nr(t.d)),P;if("EC"===t.kty&&void 0!==t.crv&&void 0!==t.x&&void 0!==t.y&&void 0===t.d){var h=(k=new s({curve:t.crv})).ecparams.keylen/4,l="04"+("0000000000"+Nr(t.x)).slice(-h)+("0000000000"+Nr(t.y)).slice(-h);return k.setPublicKeyHex(l),k}if("EC"===t.kty&&void 0!==t.crv&&void 0!==t.x&&void 0!==t.y&&void 0!==t.d){h=(k=new s({curve:t.crv})).ecparams.keylen/4,l="04"+("0000000000"+Nr(t.x)).slice(-h)+("0000000000"+Nr(t.y)).slice(-h);var p=("0000000000"+Nr(t.d)).slice(-h);return k.setPublicKeyHex(l),k.setPrivateKeyHex(p),k}if("pkcs5prv"===r){var d,g=t,v=kr;if(9===(d=n(g,0)).length)(P=new u).readPKCS5PrvKeyHex(g);else if(6===d.length)(P=new a).readPKCS5PrvKeyHex(g);else{if(!(d.length>2&&"04"===g.substr(d[1],2)))throw"unsupported PKCS#1/5 hexadecimal key";(P=new s).readPKCS5PrvKeyHex(g)}return P}if("pkcs8prv"===r)return P=f.getKeyFromPlainPrivatePKCS8Hex(t);if("pkcs8pub"===r)return f._getKeyFromPublicPKCS8Hex(t);if("x509pub"===r)return sn.getPublicKeyFromCertHex(t);if(-1!=t.indexOf("-END CERTIFICATE-",0)||-1!=t.indexOf("-END X509 CERTIFICATE-",0)||-1!=t.indexOf("-END TRUSTED CERTIFICATE-",0))return sn.getPublicKeyFromCertPEM(t);if(-1!=t.indexOf("-END PUBLIC KEY-")){var y=qr(t,"PUBLIC KEY");return f._getKeyFromPublicPKCS8Hex(y)}if(-1!=t.indexOf("-END RSA PRIVATE KEY-")&&-1==t.indexOf("4,ENCRYPTED")){var m=c(t,"RSA PRIVATE KEY");return f.getKey(m,null,"pkcs5prv")}if(-1!=t.indexOf("-END DSA PRIVATE KEY-")&&-1==t.indexOf("4,ENCRYPTED")){var _=i(R=c(t,"DSA PRIVATE KEY"),0,[1],"02"),S=i(R,0,[2],"02"),b=i(R,0,[3],"02"),w=i(R,0,[4],"02"),F=i(R,0,[5],"02");return(P=new a).setPrivate(new E(_,16),new E(S,16),new E(b,16),new E(w,16),new E(F,16)),P}if(-1!=t.indexOf("-END PRIVATE KEY-"))return f.getKeyFromPlainPrivatePKCS8PEM(t);if(-1!=t.indexOf("-END RSA PRIVATE KEY-")&&-1!=t.indexOf("4,ENCRYPTED")){var x=f.getDecryptedKeyHex(t,e),A=new qe;return A.readPKCS5PrvKeyHex(x),A}if(-1!=t.indexOf("-END EC PRIVATE KEY-")&&-1!=t.indexOf("4,ENCRYPTED")){var k,P=i(R=f.getDecryptedKeyHex(t,e),0,[1],"04"),C=i(R,0,[2,0],"06"),T=i(R,0,[3,0],"03").substr(2);if(void 0===Er.crypto.OID.oidhex2name[C])throw"undefined OID(hex) in KJUR.crypto.OID: "+C;return(k=new s({curve:Er.crypto.OID.oidhex2name[C]})).setPublicKeyHex(T),k.setPrivateKeyHex(P),k.isPublic=!1,k}if(-1!=t.indexOf("-END DSA PRIVATE KEY-")&&-1!=t.indexOf("4,ENCRYPTED")){var R;_=i(R=f.getDecryptedKeyHex(t,e),0,[1],"02"),S=i(R,0,[2],"02"),b=i(R,0,[3],"02"),w=i(R,0,[4],"02"),F=i(R,0,[5],"02");return(P=new a).setPrivate(new E(_,16),new E(S,16),new E(b,16),new E(w,16),new E(F,16)),P}if(-1!=t.indexOf("-END ENCRYPTED PRIVATE KEY-"))return f.getKeyFromEncryptedPKCS8PEM(t,e);throw"not supported argument"},tn.generateKeypair=function(t,e){if("RSA"==t){var r=e;(s=new qe).generate(r,"10001"),s.isPrivate=!0,s.isPublic=!0;var n=new qe,i=s.n.toString(16),o=s.e.toString(16);return n.setPublic(i,o),n.isPrivate=!1,n.isPublic=!0,(a={}).prvKeyObj=s,a.pubKeyObj=n,a}if("EC"==t){var s,a,u=e,c=new Er.crypto.ECDSA({curve:u}).generateKeyPairHex();return(s=new Er.crypto.ECDSA({curve:u})).setPublicKeyHex(c.ecpubhex),s.setPrivateKeyHex(c.ecprvhex),s.isPrivate=!0,s.isPublic=!1,(n=new Er.crypto.ECDSA({curve:u})).setPublicKeyHex(c.ecpubhex),n.isPrivate=!1,n.isPublic=!0,(a={}).prvKeyObj=s,a.pubKeyObj=n,a}throw"unknown algorithm: "+t},tn.getPEM=function(t,e,r,n,i,o){var s=Er,a=s.asn1,u=a.DERObjectIdentifier,c=a.DERInteger,f=a.ASN1Util.newObject,h=a.x509.SubjectPublicKeyInfo,l=s.crypto,p=l.DSA,d=l.ECDSA,g=qe;function v(t){return f({seq:[{int:0},{int:{bigint:t.n}},{int:t.e},{int:{bigint:t.d}},{int:{bigint:t.p}},{int:{bigint:t.q}},{int:{bigint:t.dmp1}},{int:{bigint:t.dmq1}},{int:{bigint:t.coeff}}]})}function m(t){return f({seq:[{int:1},{octstr:{hex:t.prvKeyHex}},{tag:["a0",!0,{oid:{name:t.curveName}}]},{tag:["a1",!0,{bitstr:{hex:"00"+t.pubKeyHex}}]}]})}function _(t){return f({seq:[{int:0},{int:{bigint:t.p}},{int:{bigint:t.q}},{int:{bigint:t.g}},{int:{bigint:t.y}},{int:{bigint:t.x}}]})}if((void 0!==g&&t instanceof g||void 0!==p&&t instanceof p||void 0!==d&&t instanceof d)&&1==t.isPublic&&(void 0===e||"PKCS8PUB"==e))return Kr(F=new h(t).getEncodedHex(),"PUBLIC KEY");if("PKCS1PRV"==e&&void 0!==g&&t instanceof g&&(void 0===r||null==r)&&1==t.isPrivate)return Kr(F=v(t).getEncodedHex(),"RSA PRIVATE KEY");if("PKCS1PRV"==e&&void 0!==d&&t instanceof d&&(void 0===r||null==r)&&1==t.isPrivate){var S=new u({name:t.curveName}).getEncodedHex(),b=m(t).getEncodedHex(),w="";return w+=Kr(S,"EC PARAMETERS"),w+=Kr(b,"EC PRIVATE KEY")}if("PKCS1PRV"==e&&void 0!==p&&t instanceof p&&(void 0===r||null==r)&&1==t.isPrivate)return Kr(F=_(t).getEncodedHex(),"DSA PRIVATE KEY");if("PKCS5PRV"==e&&void 0!==g&&t instanceof g&&void 0!==r&&null!=r&&1==t.isPrivate){var F=v(t).getEncodedHex();return void 0===n&&(n="DES-EDE3-CBC"),this.getEncryptedPKCS5PEMFromPrvKeyHex("RSA",F,r,n,o)}if("PKCS5PRV"==e&&void 0!==d&&t instanceof d&&void 0!==r&&null!=r&&1==t.isPrivate){F=m(t).getEncodedHex();return void 0===n&&(n="DES-EDE3-CBC"),this.getEncryptedPKCS5PEMFromPrvKeyHex("EC",F,r,n,o)}if("PKCS5PRV"==e&&void 0!==p&&t instanceof p&&void 0!==r&&null!=r&&1==t.isPrivate){F=_(t).getEncodedHex();return void 0===n&&(n="DES-EDE3-CBC"),this.getEncryptedPKCS5PEMFromPrvKeyHex("DSA",F,r,n,o)}var E=function t(e,r){var n=x(e,r);return new f({seq:[{seq:[{oid:{name:"pkcs5PBES2"}},{seq:[{seq:[{oid:{name:"pkcs5PBKDF2"}},{seq:[{octstr:{hex:n.pbkdf2Salt}},{int:n.pbkdf2Iter}]}]},{seq:[{oid:{name:"des-EDE3-CBC"}},{octstr:{hex:n.encryptionSchemeIV}}]}]}]},{octstr:{hex:n.ciphertext}}]}).getEncodedHex()},x=function t(e,r){var n=y.lib.WordArray.random(8),i=y.lib.WordArray.random(8),o=y.PBKDF2(r,n,{keySize:6,iterations:100}),s=y.enc.Hex.parse(e),a=y.TripleDES.encrypt(s,o,{iv:i})+"",u={};return u.ciphertext=a,u.pbkdf2Salt=y.enc.Hex.stringify(n),u.pbkdf2Iter=100,u.encryptionSchemeAlg="DES-EDE3-CBC",u.encryptionSchemeIV=y.enc.Hex.stringify(i),u};if("PKCS8PRV"==e&&void 0!=g&&t instanceof g&&1==t.isPrivate){var A=v(t).getEncodedHex();F=f({seq:[{int:0},{seq:[{oid:{name:"rsaEncryption"}},{null:!0}]},{octstr:{hex:A}}]}).getEncodedHex();return void 0===r||null==r?Kr(F,"PRIVATE KEY"):Kr(b=E(F,r),"ENCRYPTED PRIVATE KEY")}if("PKCS8PRV"==e&&void 0!==d&&t instanceof d&&1==t.isPrivate){A=new f({seq:[{int:1},{octstr:{hex:t.prvKeyHex}},{tag:["a1",!0,{bitstr:{hex:"00"+t.pubKeyHex}}]}]}).getEncodedHex(),F=f({seq:[{int:0},{seq:[{oid:{name:"ecPublicKey"}},{oid:{name:t.curveName}}]},{octstr:{hex:A}}]}).getEncodedHex();return void 0===r||null==r?Kr(F,"PRIVATE KEY"):Kr(b=E(F,r),"ENCRYPTED PRIVATE KEY")}if("PKCS8PRV"==e&&void 0!==p&&t instanceof p&&1==t.isPrivate){A=new c({bigint:t.x}).getEncodedHex(),F=f({seq:[{int:0},{seq:[{oid:{name:"dsa"}},{seq:[{int:{bigint:t.p}},{int:{bigint:t.q}},{int:{bigint:t.g}}]}]},{octstr:{hex:A}}]}).getEncodedHex();return void 0===r||null==r?Kr(F,"PRIVATE KEY"):Kr(b=E(F,r),"ENCRYPTED PRIVATE KEY")}throw"unsupported object nor format"},tn.getKeyFromCSRPEM=function(t){var e=qr(t,"CERTIFICATE REQUEST");return tn.getKeyFromCSRHex(e)},tn.getKeyFromCSRHex=function(t){var e=tn.parseCSRHex(t);return tn.getKey(e.p8pubkeyhex,null,"pkcs8pub")},tn.parseCSRHex=function(t){var e=kr,r=e.getChildIdx,n=e.getTLV,i={},o=t;if("30"!=o.substr(0,2))throw"malformed CSR(code:001)";var s=r(o,0);if(s.length<1)throw"malformed CSR(code:002)";if("30"!=o.substr(s[0],2))throw"malformed CSR(code:003)";var a=r(o,s[0]);if(a.length<3)throw"malformed CSR(code:004)";return i.p8pubkeyhex=n(o,a[2]),i},tn.getJWKFromKey=function(t){var e={};if(t instanceof qe&&t.isPrivate)return e.kty="RSA",e.n=Dr(t.n.toString(16)),e.e=Dr(t.e.toString(16)),e.d=Dr(t.d.toString(16)),e.p=Dr(t.p.toString(16)),e.q=Dr(t.q.toString(16)),e.dp=Dr(t.dmp1.toString(16)),e.dq=Dr(t.dmq1.toString(16)),e.qi=Dr(t.coeff.toString(16)),e;if(t instanceof qe&&t.isPublic)return e.kty="RSA",e.n=Dr(t.n.toString(16)),e.e=Dr(t.e.toString(16)),e;if(t instanceof Er.crypto.ECDSA&&t.isPrivate){if("P-256"!==(n=t.getShortNISTPCurveName())&&"P-384"!==n)throw"unsupported curve name for JWT: "+n;var r=t.getPublicKeyXYHex();return e.kty="EC",e.crv=n,e.x=Dr(r.x),e.y=Dr(r.y),e.d=Dr(t.prvKeyHex),e}if(t instanceof Er.crypto.ECDSA&&t.isPublic){var n;if("P-256"!==(n=t.getShortNISTPCurveName())&&"P-384"!==n)throw"unsupported curve name for JWT: "+n;r=t.getPublicKeyXYHex();return e.kty="EC",e.crv=n,e.x=Dr(r.x),e.y=Dr(r.y),e}throw"not supported key object"},qe.getPosArrayOfChildrenFromHex=function(t){return kr.getChildIdx(t,0)},qe.getHexValueArrayOfChildrenFromHex=function(t){var e,r=kr.getV,n=r(t,(e=qe.getPosArrayOfChildrenFromHex(t))[0]),i=r(t,e[1]),o=r(t,e[2]),s=r(t,e[3]),a=r(t,e[4]),u=r(t,e[5]),c=r(t,e[6]),f=r(t,e[7]),h=r(t,e[8]);return(e=new Array).push(n,i,o,s,a,u,c,f,h),e},qe.prototype.readPrivateKeyFromPEMString=function(t){var e=qr(t),r=qe.getHexValueArrayOfChildrenFromHex(e);this.setPrivateEx(r[1],r[2],r[3],r[4],r[5],r[6],r[7],r[8])},qe.prototype.readPKCS5PrvKeyHex=function(t){var e=qe.getHexValueArrayOfChildrenFromHex(t);this.setPrivateEx(e[1],e[2],e[3],e[4],e[5],e[6],e[7],e[8])},qe.prototype.readPKCS8PrvKeyHex=function(t){var e,r,n,i,o,s,a,u,c=kr,f=c.getVbyList;if(!1===c.isASN1HEX(t))throw"not ASN.1 hex string";try{e=f(t,0,[2,0,1],"02"),r=f(t,0,[2,0,2],"02"),n=f(t,0,[2,0,3],"02"),i=f(t,0,[2,0,4],"02"),o=f(t,0,[2,0,5],"02"),s=f(t,0,[2,0,6],"02"),a=f(t,0,[2,0,7],"02"),u=f(t,0,[2,0,8],"02")}catch(t){throw"malformed PKCS#8 plain RSA private key"}this.setPrivateEx(e,r,n,i,o,s,a,u)},qe.prototype.readPKCS5PubKeyHex=function(t){var e=kr,r=e.getV;if(!1===e.isASN1HEX(t))throw"keyHex is not ASN.1 hex string";var n=e.getChildIdx(t,0);if(2!==n.length||"02"!==t.substr(n[0],2)||"02"!==t.substr(n[1],2))throw"wrong hex for PKCS#5 public key";var i=r(t,n[0]),o=r(t,n[1]);this.setPublic(i,o)},qe.prototype.readPKCS8PubKeyHex=function(t){var e=kr;if(!1===e.isASN1HEX(t))throw"not ASN.1 hex string";if("06092a864886f70d010101"!==e.getTLVbyList(t,0,[0,0]))throw"not PKCS8 RSA public key";var r=e.getTLVbyList(t,0,[1,0]);this.readPKCS5PubKeyHex(r)},qe.prototype.readCertPubKeyHex=function(t,e){var r,n;(r=new sn).readCertHex(t),n=r.getPublicKeyHex(),this.readPKCS8PubKeyHex(n)};var en=new RegExp("");function rn(t,e){for(var r="",n=e/4-t.length,i=0;i>24,(16711680&i)>>16,(65280&i)>>8,255&i])))),i+=1;return n}function on(t){for(var e in Er.crypto.Util.DIGESTINFOHEAD){var r=Er.crypto.Util.DIGESTINFOHEAD[e],n=r.length;if(t.substring(0,n)==r)return[e,t.substring(n)]}return[]}function sn(){var t=kr,e=t.getChildIdx,r=t.getV,n=t.getTLV,i=t.getVbyList,o=t.getTLVbyList,s=t.getIdxbyList,a=t.getVidx,u=t.oidname,c=sn,f=qr;this.hex=null,this.version=0,this.foffset=0,this.aExtInfo=null,this.getVersion=function(){return null===this.hex||0!==this.version?this.version:"a003020102"!==o(this.hex,0,[0,0])?(this.version=1,this.foffset=-1,1):(this.version=3,3)},this.getSerialNumberHex=function(){return i(this.hex,0,[0,1+this.foffset],"02")},this.getSignatureAlgorithmField=function(){return u(i(this.hex,0,[0,2+this.foffset,0],"06"))},this.getIssuerHex=function(){return o(this.hex,0,[0,3+this.foffset],"30")},this.getIssuerString=function(){return c.hex2dn(this.getIssuerHex())},this.getSubjectHex=function(){return o(this.hex,0,[0,5+this.foffset],"30")},this.getSubjectString=function(){return c.hex2dn(this.getSubjectHex())},this.getNotBefore=function(){var t=i(this.hex,0,[0,4+this.foffset,0]);return t=t.replace(/(..)/g,"%$1"),t=decodeURIComponent(t)},this.getNotAfter=function(){var t=i(this.hex,0,[0,4+this.foffset,1]);return t=t.replace(/(..)/g,"%$1"),t=decodeURIComponent(t)},this.getPublicKeyHex=function(){return t.getTLVbyList(this.hex,0,[0,6+this.foffset],"30")},this.getPublicKeyIdx=function(){return s(this.hex,0,[0,6+this.foffset],"30")},this.getPublicKeyContentIdx=function(){var t=this.getPublicKeyIdx();return s(this.hex,t,[1,0],"30")},this.getPublicKey=function(){return tn.getKey(this.getPublicKeyHex(),null,"pkcs8pub")},this.getSignatureAlgorithmName=function(){return u(i(this.hex,0,[1,0],"06"))},this.getSignatureValueHex=function(){return i(this.hex,0,[2],"03",!0)},this.verifySignature=function(t){var e=this.getSignatureAlgorithmName(),r=this.getSignatureValueHex(),n=o(this.hex,0,[0],"30"),i=new Er.crypto.Signature({alg:e});return i.init(t),i.updateHex(n),i.verify(r)},this.parseExt=function(){if(3!==this.version)return-1;var r=s(this.hex,0,[0,7,0],"30"),n=e(this.hex,r);this.aExtInfo=new Array;for(var o=0;o0&&(c=new Array(r),(new He).nextBytes(c),c=String.fromCharCode.apply(String,c));var f=jr(u(Ur("\0\0\0\0\0\0\0\0"+i+c))),h=[];for(n=0;n>8*a-s&255;for(d[0]&=~g,n=0;nthis.n.bitLength())return 0;var n=on(this.doPublic(r).toString(16).replace(/^1f+00/,""));if(0==n.length)return!1;var i=n[0];return n[1]==function t(e){return Er.crypto.Util.hashString(e,i)}(t)},qe.prototype.verifyWithMessageHash=function(t,e){var r=Ve(e=(e=e.replace(en,"")).replace(/[ \n]+/g,""),16);if(r.bitLength()>this.n.bitLength())return 0;var n=on(this.doPublic(r).toString(16).replace(/^1f+00/,""));if(0==n.length)return!1;n[0];return n[1]==t},qe.prototype.verifyPSS=function(t,e,r,n){var i=function t(e){return Er.crypto.Util.hashHex(e,r)}(Ur(t));return void 0===n&&(n=-1),this.verifyWithMessageHashPSS(i,e,r,n)},qe.prototype.verifyWithMessageHashPSS=function(t,e,r,n){var i=new E(e,16);if(i.bitLength()>this.n.bitLength())return!1;var o,s=function t(e){return Er.crypto.Util.hashHex(e,r)},a=jr(t),u=a.length,c=this.n.bitLength()-1,f=Math.ceil(c/8);if(-1===n||void 0===n)n=u;else if(-2===n)n=f-u-2;else if(n<-2)throw"invalid salt length";if(f>8*f-c&255;if(0!=(l.charCodeAt(0)&d))throw"bits beyond keysize not zero";var g=nn(p,l.length,s),v=[];for(o=0;o0)&&-1==(":"+n.join(":")+":").indexOf(":"+v+":"))throw"algorithm '"+v+"' not accepted in the list";if("none"!=v&&null===e)throw"key shall be specified to verify.";if("string"==typeof e&&-1!=e.indexOf("-----BEGIN ")&&(e=tn.getKey(e)),!("RS"!=y&&"PS"!=y||e instanceof i))throw"key shall be a RSAKey obj for RS* and PS* algs";if("ES"==y&&!(e instanceof c))throw"key shall be a ECDSA obj for ES* algs";var m=null;if(void 0===s.jwsalg2sigalg[g.alg])throw"unsupported alg name: "+v;if("none"==(m=s.jwsalg2sigalg[v]))throw"not supported";if("Hmac"==m.substr(0,4)){if(void 0===e)throw"hexadecimal key shall be specified for HMAC";var _=new f({alg:m,pass:e});return _.updateString(p),d==_.doFinal()}if(-1!=m.indexOf("withECDSA")){var S,b=null;try{b=c.concatSigToASN1Sig(d)}catch(t){return!1}return(S=new h({alg:m})).init(e),S.updateString(p),S.verify(b)}return(S=new h({alg:m})).init(e),S.updateString(p),S.verify(d)},Er.jws.JWS.parse=function(t){var e,r,n,i=t.split("."),o={};if(2!=i.length&&3!=i.length)throw"malformed sJWS: wrong number of '.' splitted elements";return e=i[0],r=i[1],3==i.length&&(n=i[2]),o.headerObj=Er.jws.JWS.readSafeJSONString(Ar(e)),o.payloadObj=Er.jws.JWS.readSafeJSONString(Ar(r)),o.headerPP=JSON.stringify(o.headerObj,null," "),null==o.payloadObj?o.payloadPP=Ar(r):o.payloadPP=JSON.stringify(o.payloadObj,null," "),void 0!==n&&(o.sigHex=Nr(n)),o},Er.jws.JWS.verifyJWT=function(t,e,n){var i=Er.jws,o=i.JWS,s=o.readSafeJSONString,a=o.inArray,u=o.includedArray,c=t.split("."),f=c[0],h=c[1],l=(Nr(c[2]),s(Ar(f))),p=s(Ar(h));if(void 0===l.alg)return!1;if(void 0===n.alg)throw"acceptField.alg shall be specified";if(!a(l.alg,n.alg))return!1;if(void 0!==p.iss&&"object"===r(n.iss)&&!a(p.iss,n.iss))return!1;if(void 0!==p.sub&&"object"===r(n.sub)&&!a(p.sub,n.sub))return!1;if(void 0!==p.aud&&"object"===r(n.aud))if("string"==typeof p.aud){if(!a(p.aud,n.aud))return!1}else if("object"==r(p.aud)&&!u(p.aud,n.aud))return!1;var d=i.IntDate.getNow();return void 0!==n.verifyAt&&"number"==typeof n.verifyAt&&(d=n.verifyAt),void 0!==n.gracePeriod&&"number"==typeof n.gracePeriod||(n.gracePeriod=0),!(void 0!==p.exp&&"number"==typeof p.exp&&p.exp+n.gracePeriodr.length&&(n=r.length);for(var i=0;i + * @license MIT + */ +var n=r(361),i=r(362),o=r(363);function s(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function a(t,e){if(s()=s())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+s().toString(16)+" bytes");return 0|t}function d(t,e){if(u.isBuffer(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var r=t.length;if(0===r)return 0;for(var n=!1;;)switch(e){case"ascii":case"latin1":case"binary":return r;case"utf8":case"utf-8":case void 0:return V(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*r;case"hex":return r>>>1;case"base64":return K(t).length;default:if(n)return V(t).length;e=(""+e).toLowerCase(),n=!0}}function g(t,e,r){var n=t[e];t[e]=t[r],t[r]=n}function v(t,e,r,n,i){if(0===t.length)return-1;if("string"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),r=+r,isNaN(r)&&(r=i?0:t.length-1),r<0&&(r=t.length+r),r>=t.length){if(i)return-1;r=t.length-1}else if(r<0){if(!i)return-1;r=0}if("string"==typeof e&&(e=u.from(e,n)),u.isBuffer(e))return 0===e.length?-1:y(t,e,r,n,i);if("number"==typeof e)return e&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(t,e,r):Uint8Array.prototype.lastIndexOf.call(t,e,r):y(t,[e],r,n,i);throw new TypeError("val must be string, number or Buffer")}function y(t,e,r,n,i){var o,s=1,a=t.length,u=e.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||e.length<2)return-1;s=2,a/=2,u/=2,r/=2}function c(t,e){return 1===s?t[e]:t.readUInt16BE(e*s)}if(i){var f=-1;for(o=r;oa&&(r=a-u),o=r;o>=0;o--){for(var h=!0,l=0;li&&(n=i):n=i;var o=e.length;if(o%2!=0)throw new TypeError("Invalid hex string");n>o/2&&(n=o/2);for(var s=0;s>8,i=r%256,o.push(i),o.push(n);return o}(e,t.length-r),t,r,n)}function E(t,e,r){return 0===e&&r===t.length?n.fromByteArray(t):n.fromByteArray(t.slice(e,r))}function x(t,e,r){r=Math.min(t.length,r);for(var n=[],i=e;i239?4:c>223?3:c>191?2:1;if(i+h<=r)switch(h){case 1:c<128&&(f=c);break;case 2:128==(192&(o=t[i+1]))&&(u=(31&c)<<6|63&o)>127&&(f=u);break;case 3:o=t[i+1],s=t[i+2],128==(192&o)&&128==(192&s)&&(u=(15&c)<<12|(63&o)<<6|63&s)>2047&&(u<55296||u>57343)&&(f=u);break;case 4:o=t[i+1],s=t[i+2],a=t[i+3],128==(192&o)&&128==(192&s)&&128==(192&a)&&(u=(15&c)<<18|(63&o)<<12|(63&s)<<6|63&a)>65535&&u<1114112&&(f=u)}null===f?(f=65533,h=1):f>65535&&(f-=65536,n.push(f>>>10&1023|55296),f=56320|1023&f),n.push(f),i+=h}return function l(t){var e=t.length;if(e<=P)return String.fromCharCode.apply(String,t);var r="",n=0;for(;nthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return R(this,e,n);case"utf8":case"utf-8":return x(this,e,n);case"ascii":return C(this,e,n);case"latin1":case"binary":return T(this,e,n);case"base64":return E(this,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return I(this,e,n);default:if(i)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),i=!0}}.apply(this,arguments)},u.prototype.equals=function t(e){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===u.compare(this,e)},u.prototype.inspect=function t(){var r="",n=e.INSPECT_MAX_BYTES;return this.length>0&&(r=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(r+=" ... ")),""},u.prototype.compare=function t(e,r,n,i,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===r&&(r=0),void 0===n&&(n=e?e.length:0),void 0===i&&(i=0),void 0===o&&(o=this.length),r<0||n>e.length||i<0||o>this.length)throw new RangeError("out of range index");if(i>=o&&r>=n)return 0;if(i>=o)return-1;if(r>=n)return 1;if(r>>>=0,n>>>=0,i>>>=0,o>>>=0,this===e)return 0;for(var s=o-i,a=n-r,c=Math.min(s,a),f=this.slice(i,o),h=e.slice(r,n),l=0;lo)&&(n=o),e.length>0&&(n<0||r<0)||r>this.length)throw new RangeError("Attempt to write outside buffer bounds");i||(i="utf8");for(var s=!1;;)switch(i){case"hex":return m(this,e,r,n);case"utf8":case"utf-8":return _(this,e,r,n);case"ascii":return S(this,e,r,n);case"latin1":case"binary":return b(this,e,r,n);case"base64":return w(this,e,r,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return F(this,e,r,n);default:if(s)throw new TypeError("Unknown encoding: "+i);i=(""+i).toLowerCase(),s=!0}},u.prototype.toJSON=function t(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var P=4096;function C(t,e,r){var n="";r=Math.min(t.length,r);for(var i=e;in)&&(r=n);for(var i="",o=e;or)throw new RangeError("Trying to access beyond buffer length")}function D(t,e,r,n,i,o){if(!u.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>i||et.length)throw new RangeError("Index out of range")}function N(t,e,r,n){e<0&&(e=65535+e+1);for(var i=0,o=Math.min(t.length-r,2);i>>8*(n?i:1-i)}function L(t,e,r,n){e<0&&(e=4294967295+e+1);for(var i=0,o=Math.min(t.length-r,4);i>>8*(n?i:3-i)&255}function M(t,e,r,n,i,o){if(r+n>t.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("Index out of range")}function j(t,e,r,n,o){return o||M(t,0,r,4),i.write(t,e,r,n,23,4),r+4}function U(t,e,r,n,o){return o||M(t,0,r,8),i.write(t,e,r,n,52,8),r+8}u.prototype.slice=function t(e,r){var n,i=this.length;if(e=~~e,r=void 0===r?i:~~r,e<0?(e+=i)<0&&(e=0):e>i&&(e=i),r<0?(r+=i)<0&&(r=0):r>i&&(r=i),r0&&(o*=256);)i+=this[e+--r]*o;return i},u.prototype.readUInt8=function t(e,r){return r||O(e,1,this.length),this[e]},u.prototype.readUInt16LE=function t(e,r){return r||O(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function t(e,r){return r||O(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function t(e,r){return r||O(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function t(e,r){return r||O(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function t(e,r,n){e|=0,r|=0,n||O(e,r,this.length);for(var i=this[e],o=1,s=0;++s=(o*=128)&&(i-=Math.pow(2,8*r)),i},u.prototype.readIntBE=function t(e,r,n){e|=0,r|=0,n||O(e,r,this.length);for(var i=r,o=1,s=this[e+--i];i>0&&(o*=256);)s+=this[e+--i]*o;return s>=(o*=128)&&(s-=Math.pow(2,8*r)),s},u.prototype.readInt8=function t(e,r){return r||O(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function t(e,r){r||O(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function t(e,r){r||O(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function t(e,r){return r||O(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function t(e,r){return r||O(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function t(e,r){return r||O(e,4,this.length),i.read(this,e,!0,23,4)},u.prototype.readFloatBE=function t(e,r){return r||O(e,4,this.length),i.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function t(e,r){return r||O(e,8,this.length),i.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function t(e,r){return r||O(e,8,this.length),i.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function t(e,r,n,i){(e=+e,r|=0,n|=0,i)||D(this,e,r,n,Math.pow(2,8*n)-1,0);var o=1,s=0;for(this[r]=255&e;++s=0&&(s*=256);)this[r+o]=e/s&255;return r+n},u.prototype.writeUInt8=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[r]=255&e,r+1},u.prototype.writeUInt16LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[r]=255&e,this[r+1]=e>>>8):N(this,e,r,!0),r+2},u.prototype.writeUInt16BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>8,this[r+1]=255&e):N(this,e,r,!1),r+2},u.prototype.writeUInt32LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[r+3]=e>>>24,this[r+2]=e>>>16,this[r+1]=e>>>8,this[r]=255&e):L(this,e,r,!0),r+4},u.prototype.writeUInt32BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>24,this[r+1]=e>>>16,this[r+2]=e>>>8,this[r+3]=255&e):L(this,e,r,!1),r+4},u.prototype.writeIntLE=function t(e,r,n,i){if(e=+e,r|=0,!i){var o=Math.pow(2,8*n-1);D(this,e,r,n,o-1,-o)}var s=0,a=1,u=0;for(this[r]=255&e;++s>0)-u&255;return r+n},u.prototype.writeIntBE=function t(e,r,n,i){if(e=+e,r|=0,!i){var o=Math.pow(2,8*n-1);D(this,e,r,n,o-1,-o)}var s=n-1,a=1,u=0;for(this[r+s]=255&e;--s>=0&&(a*=256);)e<0&&0===u&&0!==this[r+s+1]&&(u=1),this[r+s]=(e/a>>0)-u&255;return r+n},u.prototype.writeInt8=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[r]=255&e,r+1},u.prototype.writeInt16LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[r]=255&e,this[r+1]=e>>>8):N(this,e,r,!0),r+2},u.prototype.writeInt16BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>8,this[r+1]=255&e):N(this,e,r,!1),r+2},u.prototype.writeInt32LE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[r]=255&e,this[r+1]=e>>>8,this[r+2]=e>>>16,this[r+3]=e>>>24):L(this,e,r,!0),r+4},u.prototype.writeInt32BE=function t(e,r,n){return e=+e,r|=0,n||D(this,e,r,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[r]=e>>>24,this[r+1]=e>>>16,this[r+2]=e>>>8,this[r+3]=255&e):L(this,e,r,!1),r+4},u.prototype.writeFloatLE=function t(e,r,n){return j(this,e,r,!0,n)},u.prototype.writeFloatBE=function t(e,r,n){return j(this,e,r,!1,n)},u.prototype.writeDoubleLE=function t(e,r,n){return U(this,e,r,!0,n)},u.prototype.writeDoubleBE=function t(e,r,n){return U(this,e,r,!1,n)},u.prototype.copy=function t(e,r,n,i){if(n||(n=0),i||0===i||(i=this.length),r>=e.length&&(r=e.length),r||(r=0),i>0&&i=this.length)throw new RangeError("sourceStart out of bounds");if(i<0)throw new RangeError("sourceEnd out of bounds");i>this.length&&(i=this.length),e.length-r=0;--o)e[o+r]=this[o+n];else if(s<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(s=r;s55295&&r<57344){if(!i){if(r>56319){(e-=3)>-1&&o.push(239,191,189);continue}if(s+1===n){(e-=3)>-1&&o.push(239,191,189);continue}i=r;continue}if(r<56320){(e-=3)>-1&&o.push(239,191,189),i=r;continue}r=65536+(i-55296<<10|r-56320)}else i&&(e-=3)>-1&&o.push(239,191,189);if(i=null,r<128){if((e-=1)<0)break;o.push(r)}else if(r<2048){if((e-=2)<0)break;o.push(r>>6|192,63&r|128)}else if(r<65536){if((e-=3)<0)break;o.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;o.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return o}function K(t){return n.toByteArray(function e(t){if((t=function e(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}(t).replace(B,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function q(t,e,r,n){for(var i=0;i=e.length||i>=t.length);++i)e[i+r]=t[i];return i}}).call(this,r(71))},function(t,e,r){"use strict";e.byteLength=function n(t){var e=l(t),r=e[0],n=e[1];return 3*(r+n)/4-n},e.toByteArray=function i(t){for(var e,r=l(t),n=r[0],i=r[1],o=new u(function s(t,e,r){return 3*(e+r)/4-r}(0,n,i)),c=0,f=i>0?n-4:n,h=0;h>16&255,o[c++]=e>>8&255,o[c++]=255&e;2===i&&(e=a[t.charCodeAt(h)]<<2|a[t.charCodeAt(h+1)]>>4,o[c++]=255&e);1===i&&(e=a[t.charCodeAt(h)]<<10|a[t.charCodeAt(h+1)]<<4|a[t.charCodeAt(h+2)]>>2,o[c++]=e>>8&255,o[c++]=255&e);return o},e.fromByteArray=function o(t){for(var e,r=t.length,n=r%3,i=[],o=0,a=r-n;oa?a:o+16383));1===n?(e=t[r-1],i.push(s[e>>2]+s[e<<4&63]+"==")):2===n&&(e=(t[r-2]<<8)+t[r-1],i.push(s[e>>10]+s[e>>4&63]+s[e<<2&63]+"="));return i.join("")};for(var s=[],a=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",f=0,h=c.length;f0)throw new Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function p(t,e,r){for(var n,i,o=[],a=e;a>18&63]+s[i>>12&63]+s[i>>6&63]+s[63&i]);return o.join("")}a["-".charCodeAt(0)]=62,a["_".charCodeAt(0)]=63},function(t,e){e.read=function(t,e,r,n,i){var o,s,a=8*i-n-1,u=(1<>1,f=-7,h=r?i-1:0,l=r?-1:1,p=t[e+h];for(h+=l,o=p&(1<<-f)-1,p>>=-f,f+=a;f>0;o=256*o+t[e+h],h+=l,f-=8);for(s=o&(1<<-f)-1,o>>=-f,f+=n;f>0;s=256*s+t[e+h],h+=l,f-=8);if(0===o)o=1-c;else{if(o===u)return s?NaN:1/0*(p?-1:1);s+=Math.pow(2,n),o-=c}return(p?-1:1)*s*Math.pow(2,o-n)},e.write=function(t,e,r,n,i,o){var s,a,u,c=8*o-i-1,f=(1<>1,l=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=n?0:o-1,d=n?1:-1,g=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(a=isNaN(e)?1:0,s=f):(s=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-s))<1&&(s--,u*=2),(e+=s+h>=1?l/u:l*Math.pow(2,1-h))*u>=2&&(s++,u/=2),s+h>=f?(a=0,s=f):s+h>=1?(a=(e*u-1)*Math.pow(2,i),s+=h):(a=e*Math.pow(2,h-1)*Math.pow(2,i),s=0));i>=8;t[r+p]=255&a,p+=d,a/=256,i-=8);for(s=s<0;t[r+p]=255&s,p+=d,s/=256,c-=8);t[r+p-d]|=128*g}},function(t,e){var r={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==r.call(t)}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default=function n(t){var e=t.jws,r=t.KeyUtil,n=t.X509,o=t.crypto,s=t.hextob64u,a=t.b64tohex,u=t.AllowedSigningAlgs;return function(){function t(){!function e(t,r){if(!(t instanceof r))throw new TypeError("Cannot call a class as a function")}(this,t)}return t.parseJwt=function t(r){i.Log.debug("JoseUtil.parseJwt");try{var n=e.JWS.parse(r);return{header:n.headerObj,payload:n.payloadObj}}catch(t){i.Log.error(t)}},t.validateJwt=function e(o,s,u,c,f,h,l){i.Log.debug("JoseUtil.validateJwt");try{if("RSA"===s.kty)if(s.e&&s.n)s=r.getKey(s);else{if(!s.x5c||!s.x5c.length)return i.Log.error("JoseUtil.validateJwt: RSA key missing key material",s),Promise.reject(new Error("RSA key missing key material"));var p=a(s.x5c[0]);s=n.getPublicKeyFromCertHex(p)}else{if("EC"!==s.kty)return i.Log.error("JoseUtil.validateJwt: Unsupported key type",s&&s.kty),Promise.reject(new Error(s.kty));if(!(s.crv&&s.x&&s.y))return i.Log.error("JoseUtil.validateJwt: EC key missing key material",s),Promise.reject(new Error("EC key missing key material"));s=r.getKey(s)}return t._validateJwt(o,s,u,c,f,h,l)}catch(t){return i.Log.error(t&&t.message||t),Promise.reject("JWT validation failed")}},t.validateJwtAttributes=function e(r,n,o,s,a,u){s||(s=0),a||(a=parseInt(Date.now()/1e3));var c=t.parseJwt(r).payload;if(!c.iss)return i.Log.error("JoseUtil._validateJwt: issuer was not provided"),Promise.reject(new Error("issuer was not provided"));if(c.iss!==n)return i.Log.error("JoseUtil._validateJwt: Invalid issuer in token",c.iss),Promise.reject(new Error("Invalid issuer in token: "+c.iss));if(!c.aud)return i.Log.error("JoseUtil._validateJwt: aud was not provided"),Promise.reject(new Error("aud was not provided"));var f=c.aud===o||Array.isArray(c.aud)&&c.aud.indexOf(o)>=0;if(!f)return i.Log.error("JoseUtil._validateJwt: Invalid audience in token",c.aud),Promise.reject(new Error("Invalid audience in token: "+c.aud));if(c.azp&&c.azp!==o)return i.Log.error("JoseUtil._validateJwt: Invalid azp in token",c.azp),Promise.reject(new Error("Invalid azp in token: "+c.azp));if(!u){var h=a+s,l=a-s;if(!c.iat)return i.Log.error("JoseUtil._validateJwt: iat was not provided"),Promise.reject(new Error("iat was not provided"));if(h>>((3&r)<<3)&255;return i}}},function(t,e){for(var r=[],n=0;n<256;++n)r[n]=(n+256).toString(16).substr(1);t.exports=function i(t,e){var n=e||0,i=r;return[i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],"-",i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]],i[t[n++]]].join("")}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SigninResponse=void 0;var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:"#";!function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t);var o=i.UrlUtility.parseUrlFragment(e,r);this.error=o.error,this.error_description=o.error_description,this.error_uri=o.error_uri,this.code=o.code,this.state=o.state,this.id_token=o.id_token,this.session_state=o.session_state,this.access_token=o.access_token,this.token_type=o.token_type,this.scope=o.scope,this.profile=void 0,this.expires_in=o.expires_in}return n(t,[{key:"expires_in",get:function t(){if(this.expires_at){var e=parseInt(Date.now()/1e3);return this.expires_at-e}},set:function t(e){var r=parseInt(e);if("number"==typeof r&&r>0){var n=parseInt(Date.now()/1e3);this.expires_at=n+r}}},{key:"expired",get:function t(){var e=this.expires_in;if(void 0!==e)return e<=0}},{key:"scopes",get:function t(){return(this.scope||"").split(" ")}},{key:"isOpenIdConnect",get:function t(){return this.scopes.indexOf("openid")>=0||!!this.id_token}}]),t}()},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SignoutRequest=void 0;var n=r(3),i=r(55),o=r(102);e.SignoutRequest=function t(e){var r=e.url,s=e.id_token_hint,a=e.post_logout_redirect_uri,u=e.data,c=e.extraQueryParams,f=e.request_type;if(function h(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),!r)throw n.Log.error("SignoutRequest.ctor: No url passed"),new Error("url");for(var l in s&&(r=i.UrlUtility.addQueryParam(r,"id_token_hint",s)),a&&(r=i.UrlUtility.addQueryParam(r,"post_logout_redirect_uri",a),u&&(this.state=new o.State({data:u,request_type:f}),r=i.UrlUtility.addQueryParam(r,"state",this.state.id))),c)r=i.UrlUtility.addQueryParam(r,l,c[l]);this.url=r}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SignoutResponse=void 0;var n=r(55);e.SignoutResponse=function t(e){!function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t);var i=n.UrlUtility.parseUrlFragment(e,"?");this.error=i.error,this.error_description=i.error_description,this.error_uri=i.error_uri,this.state=i.state}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InMemoryWebStorage=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:c.SilentRenewService,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f.SessionMonitor,a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:h.TokenRevocationClient,d=arguments.length>4&&void 0!==arguments[4]?arguments[4]:l.TokenClient,g=arguments.length>5&&void 0!==arguments[5]?arguments[5]:p.JoseUtil;!function v(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),r instanceof s.UserManagerSettings||(r=new s.UserManagerSettings(r));var y=function m(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,r));return y._events=new u.UserManagerEvents(r),y._silentRenewService=new n(y),y.settings.automaticSilentRenew&&(i.Log.debug("UserManager.ctor: automaticSilentRenew is configured, setting up silent renew"),y.startSilentRenew()),y.settings.monitorSession&&(i.Log.debug("UserManager.ctor: monitorSession is configured, setting up session monitor"),y._sessionMonitor=new o(y)),y._tokenRevocationClient=new a(y._settings),y._tokenClient=new d(y._settings),y._joseUtil=g,y}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype.getUser=function t(){var e=this;return this._loadUser().then(function(t){return t?(i.Log.info("UserManager.getUser: user loaded"),e._events.load(t,!1),t):(i.Log.info("UserManager.getUser: user not found in storage"),null)})},e.prototype.removeUser=function t(){var e=this;return this.storeUser(null).then(function(){i.Log.info("UserManager.removeUser: user removed from storage"),e._events.unload()})},e.prototype.signinRedirect=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="si:r";var r={useReplaceToNavigate:e.useReplaceToNavigate};return this._signinStart(e,this._redirectNavigator,r).then(function(){i.Log.info("UserManager.signinRedirect: successful")})},e.prototype.signinRedirectCallback=function t(e){return this._signinEnd(e||this._redirectNavigator.url).then(function(t){return t.profile&&t.profile.sub?i.Log.info("UserManager.signinRedirectCallback: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinRedirectCallback: no sub"),t})},e.prototype.signinPopup=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="si:p";var r=e.redirect_uri||this.settings.popup_redirect_uri||this.settings.redirect_uri;return r?(e.redirect_uri=r,e.display="popup",this._signin(e,this._popupNavigator,{startUrl:r,popupWindowFeatures:e.popupWindowFeatures||this.settings.popupWindowFeatures,popupWindowTarget:e.popupWindowTarget||this.settings.popupWindowTarget}).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinPopup: signinPopup successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinPopup: no sub")),t})):(i.Log.error("UserManager.signinPopup: No popup_redirect_uri or redirect_uri configured"),Promise.reject(new Error("No popup_redirect_uri or redirect_uri configured")))},e.prototype.signinPopupCallback=function t(e){return this._signinCallback(e,this._popupNavigator).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinPopupCallback: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinPopupCallback: no sub")),t}).catch(function(t){i.Log.error(t.message)})},e.prototype.signinSilent=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(r=Object.assign({},r)).request_type="si:s",this._loadUser().then(function(t){return t&&t.refresh_token?(r.refresh_token=t.refresh_token,e._useRefreshToken(r)):(r.id_token_hint=r.id_token_hint||e.settings.includeIdTokenInSilentRenew&&t&&t.id_token,t&&e._settings.validateSubOnSilentRenew&&(i.Log.debug("UserManager.signinSilent, subject prior to silent renew: ",t.profile.sub),r.current_sub=t.profile.sub),e._signinSilentIframe(r))})},e.prototype._useRefreshToken=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this._tokenClient.exchangeRefreshToken(r).then(function(t){return t?t.access_token?e._loadUser().then(function(r){if(r){var n=Promise.resolve();return t.id_token&&(n=e._validateIdTokenFromTokenRefreshToken(r.profile,t.id_token)),n.then(function(){return i.Log.debug("UserManager._useRefreshToken: refresh token response success"),r.id_token=t.id_token,r.access_token=t.access_token,r.refresh_token=t.refresh_token||r.refresh_token,r.expires_in=t.expires_in,e.storeUser(r).then(function(){return e._events.load(r),r})})}return null}):(i.Log.error("UserManager._useRefreshToken: No access token returned from token endpoint"),Promise.reject("No access token returned from token endpoint")):(i.Log.error("UserManager._useRefreshToken: No response returned from token endpoint"),Promise.reject("No response returned from token endpoint"))})},e.prototype._validateIdTokenFromTokenRefreshToken=function t(e,r){var n=this;return this._metadataService.getIssuer().then(function(t){return n._joseUtil.validateJwtAttributes(r,t,n._settings.client_id,n._settings.clockSkew).then(function(t){return t?t.sub!==e.sub?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: sub in id_token does not match current sub"),Promise.reject(new Error("sub in id_token does not match current sub"))):t.auth_time&&t.auth_time!==e.auth_time?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: auth_time in id_token does not match original auth_time"),Promise.reject(new Error("auth_time in id_token does not match original auth_time"))):t.azp&&t.azp!==e.azp?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp in id_token does not match original azp"),Promise.reject(new Error("azp in id_token does not match original azp"))):!t.azp&&e.azp?(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp not in id_token, but present in original id_token"),Promise.reject(new Error("azp not in id_token, but present in original id_token"))):void 0:(i.Log.error("UserManager._validateIdTokenFromTokenRefreshToken: Failed to validate id_token"),Promise.reject(new Error("Failed to validate id_token")))})})},e.prototype._signinSilentIframe=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=e.redirect_uri||this.settings.silent_redirect_uri||this.settings.redirect_uri;return r?(e.redirect_uri=r,e.prompt=e.prompt||"none",this._signin(e,this._iframeNavigator,{startUrl:r,silentRequestTimeout:e.silentRequestTimeout||this.settings.silentRequestTimeout}).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinSilent: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinSilent: no sub")),t})):(i.Log.error("UserManager.signinSilent: No silent_redirect_uri configured"),Promise.reject(new Error("No silent_redirect_uri configured")))},e.prototype.signinSilentCallback=function t(e){return this._signinCallback(e,this._iframeNavigator).then(function(t){return t&&(t.profile&&t.profile.sub?i.Log.info("UserManager.signinSilentCallback: successful, signed in sub: ",t.profile.sub):i.Log.info("UserManager.signinSilentCallback: no sub")),t})},e.prototype.signinCallback=function t(e){var r=this;return this.readSigninResponseState(e).then(function(t){var n=t.state;t.response;return"si:r"===n.request_type?r.signinRedirectCallback(e):"si:p"===n.request_type?r.signinPopupCallback(e):"si:s"===n.request_type?r.signinSilentCallback(e):Promise.reject(new Error("invalid response_type in state"))})},e.prototype.signoutCallback=function t(e,r){var n=this;return this.readSignoutResponseState(e).then(function(t){var i=t.state,o=t.response;return i?"so:r"===i.request_type?n.signoutRedirectCallback(e):"so:p"===i.request_type?n.signoutPopupCallback(e,r):Promise.reject(new Error("invalid response_type in state")):o})},e.prototype.querySessionStatus=function t(){var e=this,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(r=Object.assign({},r)).request_type="si:s";var n=r.redirect_uri||this.settings.silent_redirect_uri||this.settings.redirect_uri;return n?(r.redirect_uri=n,r.prompt="none",r.response_type=r.response_type||this.settings.query_status_response_type,r.scope=r.scope||"openid",r.skipUserInfo=!0,this._signinStart(r,this._iframeNavigator,{startUrl:n,silentRequestTimeout:r.silentRequestTimeout||this.settings.silentRequestTimeout}).then(function(t){return e.processSigninResponse(t.url).then(function(t){if(i.Log.debug("UserManager.querySessionStatus: got signin response"),t.session_state&&t.profile.sub)return i.Log.info("UserManager.querySessionStatus: querySessionStatus success for sub: ",t.profile.sub),{session_state:t.session_state,sub:t.profile.sub,sid:t.profile.sid};i.Log.info("querySessionStatus successful, user not authenticated")})})):(i.Log.error("UserManager.querySessionStatus: No silent_redirect_uri configured"),Promise.reject(new Error("No silent_redirect_uri configured")))},e.prototype._signin=function t(e,r){var n=this,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this._signinStart(e,r,i).then(function(t){return n._signinEnd(t.url,e)})},e.prototype._signinStart=function t(e,r){var n=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return r.prepare(o).then(function(t){return i.Log.debug("UserManager._signinStart: got navigator window handle"),n.createSigninRequest(e).then(function(e){return i.Log.debug("UserManager._signinStart: got signin request"),o.url=e.url,o.id=e.state.id,t.navigate(o)}).catch(function(e){throw t.close&&(i.Log.debug("UserManager._signinStart: Error after preparing navigator, closing navigator window"),t.close()),e})})},e.prototype._signinEnd=function t(e){var r=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return this.processSigninResponse(e).then(function(t){i.Log.debug("UserManager._signinEnd: got signin response");var e=new a.User(t);if(n.current_sub){if(n.current_sub!==e.profile.sub)return i.Log.debug("UserManager._signinEnd: current user does not match user returned from signin. sub from signin: ",e.profile.sub),Promise.reject(new Error("login_required"));i.Log.debug("UserManager._signinEnd: current user matches user returned from signin")}return r.storeUser(e).then(function(){return i.Log.debug("UserManager._signinEnd: user stored"),r._events.load(e),e})})},e.prototype._signinCallback=function t(e,r){return i.Log.debug("UserManager._signinCallback"),r.callback(e)},e.prototype.signoutRedirect=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="so:r";var r=e.post_logout_redirect_uri||this.settings.post_logout_redirect_uri;r&&(e.post_logout_redirect_uri=r);var n={useReplaceToNavigate:e.useReplaceToNavigate};return this._signoutStart(e,this._redirectNavigator,n).then(function(){i.Log.info("UserManager.signoutRedirect: successful")})},e.prototype.signoutRedirectCallback=function t(e){return this._signoutEnd(e||this._redirectNavigator.url).then(function(t){return i.Log.info("UserManager.signoutRedirectCallback: successful"),t})},e.prototype.signoutPopup=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(e=Object.assign({},e)).request_type="so:p";var r=e.post_logout_redirect_uri||this.settings.popup_post_logout_redirect_uri||this.settings.post_logout_redirect_uri;return e.post_logout_redirect_uri=r,e.display="popup",e.post_logout_redirect_uri&&(e.state=e.state||{}),this._signout(e,this._popupNavigator,{startUrl:r,popupWindowFeatures:e.popupWindowFeatures||this.settings.popupWindowFeatures,popupWindowTarget:e.popupWindowTarget||this.settings.popupWindowTarget}).then(function(){i.Log.info("UserManager.signoutPopup: successful")})},e.prototype.signoutPopupCallback=function t(e,r){void 0===r&&"boolean"==typeof e&&(r=e,e=null);return this._popupNavigator.callback(e,r,"?").then(function(){i.Log.info("UserManager.signoutPopupCallback: successful")})},e.prototype._signout=function t(e,r){var n=this,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this._signoutStart(e,r,i).then(function(t){return n._signoutEnd(t.url)})},e.prototype._signoutStart=function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=this,n=arguments[1],o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.prepare(o).then(function(t){return i.Log.debug("UserManager._signoutStart: got navigator window handle"),r._loadUser().then(function(n){return i.Log.debug("UserManager._signoutStart: loaded current user from storage"),(r._settings.revokeAccessTokenOnSignout?r._revokeInternal(n):Promise.resolve()).then(function(){var s=e.id_token_hint||n&&n.id_token;return s&&(i.Log.debug("UserManager._signoutStart: Setting id_token into signout request"),e.id_token_hint=s),r.removeUser().then(function(){return i.Log.debug("UserManager._signoutStart: user removed, creating signout request"),r.createSignoutRequest(e).then(function(e){return i.Log.debug("UserManager._signoutStart: got signout request"),o.url=e.url,e.state&&(o.id=e.state.id),t.navigate(o)})})})}).catch(function(e){throw t.close&&(i.Log.debug("UserManager._signoutStart: Error after preparing navigator, closing navigator window"),t.close()),e})})},e.prototype._signoutEnd=function t(e){return this.processSignoutResponse(e).then(function(t){return i.Log.debug("UserManager._signoutEnd: got signout response"),t})},e.prototype.revokeAccessToken=function t(){var e=this;return this._loadUser().then(function(t){return e._revokeInternal(t,!0).then(function(r){if(r)return i.Log.debug("UserManager.revokeAccessToken: removing token properties from user and re-storing"),t.access_token=null,t.refresh_token=null,t.expires_at=null,t.token_type=null,e.storeUser(t).then(function(){i.Log.debug("UserManager.revokeAccessToken: user stored"),e._events.load(t)})})}).then(function(){i.Log.info("UserManager.revokeAccessToken: access token revoked successfully")})},e.prototype._revokeInternal=function t(e,r){var n=this;if(e){var o=e.access_token,s=e.refresh_token;return this._revokeAccessTokenInternal(o,r).then(function(t){return n._revokeRefreshTokenInternal(s,r).then(function(e){return t||e||i.Log.debug("UserManager.revokeAccessToken: no need to revoke due to no token(s), or JWT format"),t||e})})}return Promise.resolve(!1)},e.prototype._revokeAccessTokenInternal=function t(e,r){return!e||e.indexOf(".")>=0?Promise.resolve(!1):this._tokenRevocationClient.revoke(e,r).then(function(){return!0})},e.prototype._revokeRefreshTokenInternal=function t(e,r){return e?this._tokenRevocationClient.revoke(e,r,"refresh_token").then(function(){return!0}):Promise.resolve(!1)},e.prototype.startSilentRenew=function t(){this._silentRenewService.start()},e.prototype.stopSilentRenew=function t(){this._silentRenewService.stop()},e.prototype._loadUser=function t(){return this._userStore.get(this._userStoreKey).then(function(t){return t?(i.Log.debug("UserManager._loadUser: user storageString loaded"),a.User.fromStorageString(t)):(i.Log.debug("UserManager._loadUser: no user storageString"),null)})},e.prototype.storeUser=function t(e){if(e){i.Log.debug("UserManager.storeUser: storing user");var r=e.toStorageString();return this._userStore.set(this._userStoreKey,r)}return i.Log.debug("storeUser.storeUser: removing user"),this._userStore.remove(this._userStoreKey)},n(e,[{key:"_redirectNavigator",get:function t(){return this.settings.redirectNavigator}},{key:"_popupNavigator",get:function t(){return this.settings.popupNavigator}},{key:"_iframeNavigator",get:function t(){return this.settings.iframeNavigator}},{key:"_userStore",get:function t(){return this.settings.userStore}},{key:"events",get:function t(){return this._events}},{key:"_userStoreKey",get:function t(){return"user:"+this.settings.authority+":"+this.settings.client_id}}]),e}(o.OidcClient)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UserManagerSettings=void 0;var n=function(){function t(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},n=r.popup_redirect_uri,i=r.popup_post_logout_redirect_uri,p=r.popupWindowFeatures,d=r.popupWindowTarget,g=r.silent_redirect_uri,v=r.silentRequestTimeout,y=r.automaticSilentRenew,m=void 0!==y&&y,_=r.validateSubOnSilentRenew,S=void 0!==_&&_,b=r.includeIdTokenInSilentRenew,w=void 0===b||b,F=r.monitorSession,E=void 0===F||F,x=r.checkSessionInterval,A=void 0===x?l:x,k=r.stopCheckSessionOnError,P=void 0===k||k,C=r.query_status_response_type,T=r.revokeAccessTokenOnSignout,R=void 0!==T&&T,I=r.accessTokenExpiringNotificationTime,O=void 0===I?h:I,D=r.redirectNavigator,N=void 0===D?new o.RedirectNavigator:D,L=r.popupNavigator,M=void 0===L?new s.PopupNavigator:L,j=r.iframeNavigator,U=void 0===j?new a.IFrameNavigator:j,B=r.userStore,H=void 0===B?new u.WebStorageStateStore({store:c.Global.sessionStorage}):B;!function V(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e);var K=function q(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,arguments[0]));return K._popup_redirect_uri=n,K._popup_post_logout_redirect_uri=i,K._popupWindowFeatures=p,K._popupWindowTarget=d,K._silent_redirect_uri=g,K._silentRequestTimeout=v,K._automaticSilentRenew=m,K._validateSubOnSilentRenew=S,K._includeIdTokenInSilentRenew=w,K._accessTokenExpiringNotificationTime=O,K._monitorSession=E,K._checkSessionInterval=A,K._stopCheckSessionOnError=P,C?K._query_status_response_type=C:arguments[0]&&arguments[0].response_type?K._query_status_response_type=f.SigninRequest.isOidc(arguments[0].response_type)?"id_token":"code":K._query_status_response_type="id_token",K._revokeAccessTokenOnSignout=R,K._redirectNavigator=N,K._popupNavigator=M,K._iframeNavigator=U,K._userStore=H,K}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),n(e,[{key:"popup_redirect_uri",get:function t(){return this._popup_redirect_uri}},{key:"popup_post_logout_redirect_uri",get:function t(){return this._popup_post_logout_redirect_uri}},{key:"popupWindowFeatures",get:function t(){return this._popupWindowFeatures}},{key:"popupWindowTarget",get:function t(){return this._popupWindowTarget}},{key:"silent_redirect_uri",get:function t(){return this._silent_redirect_uri}},{key:"silentRequestTimeout",get:function t(){return this._silentRequestTimeout}},{key:"automaticSilentRenew",get:function t(){return this._automaticSilentRenew}},{key:"validateSubOnSilentRenew",get:function t(){return this._validateSubOnSilentRenew}},{key:"includeIdTokenInSilentRenew",get:function t(){return this._includeIdTokenInSilentRenew}},{key:"accessTokenExpiringNotificationTime",get:function t(){return this._accessTokenExpiringNotificationTime}},{key:"monitorSession",get:function t(){return this._monitorSession}},{key:"checkSessionInterval",get:function t(){return this._checkSessionInterval}},{key:"stopCheckSessionOnError",get:function t(){return this._stopCheckSessionOnError}},{key:"query_status_response_type",get:function t(){return this._query_status_response_type}},{key:"revokeAccessTokenOnSignout",get:function t(){return this._revokeAccessTokenOnSignout}},{key:"redirectNavigator",get:function t(){return this._redirectNavigator}},{key:"popupNavigator",get:function t(){return this._popupNavigator}},{key:"iframeNavigator",get:function t(){return this._iframeNavigator}},{key:"userStore",get:function t(){return this._userStore}}]),e}(i.OidcClientSettings)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RedirectNavigator=void 0;var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1])||arguments[1];n.Log.debug("UserManagerEvents.load"),t.prototype.load.call(this,r),i&&this._userLoaded.raise(r)},e.prototype.unload=function e(){n.Log.debug("UserManagerEvents.unload"),t.prototype.unload.call(this),this._userUnloaded.raise()},e.prototype.addUserLoaded=function t(e){this._userLoaded.addHandler(e)},e.prototype.removeUserLoaded=function t(e){this._userLoaded.removeHandler(e)},e.prototype.addUserUnloaded=function t(e){this._userUnloaded.addHandler(e)},e.prototype.removeUserUnloaded=function t(e){this._userUnloaded.removeHandler(e)},e.prototype.addSilentRenewError=function t(e){this._silentRenewError.addHandler(e)},e.prototype.removeSilentRenewError=function t(e){this._silentRenewError.removeHandler(e)},e.prototype._raiseSilentRenewError=function t(e){n.Log.debug("UserManagerEvents._raiseSilentRenewError",e.message),this._silentRenewError.raise(e)},e.prototype.addUserSignedOut=function t(e){this._userSignedOut.addHandler(e)},e.prototype.removeUserSignedOut=function t(e){this._userSignedOut.removeHandler(e)},e.prototype._raiseUserSignedOut=function t(){n.Log.debug("UserManagerEvents._raiseUserSignedOut"),this._userSignedOut.raise()},e.prototype.addUserSessionChanged=function t(e){this._userSessionChanged.addHandler(e)},e.prototype.removeUserSessionChanged=function t(e){this._userSessionChanged.removeHandler(e)},e.prototype._raiseUserSessionChanged=function t(){n.Log.debug("UserManagerEvents._raiseUserSessionChanged"),this._userSessionChanged.raise()},e}(i.AccessTokenEvents)},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Timer=void 0;var n=function(){function t(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:o.Global.timer,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;!function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e);var a=function u(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,r));return a._timer=n,a._nowFunc=i||function(){return Date.now()/1e3},a}return function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype.init=function t(e){e<=0&&(e=1),e=parseInt(e);var r=this.now+e;if(this.expiration===r&&this._timerHandle)i.Log.debug("Timer.init timer "+this._name+" skipping initialization since already initialized for expiration:",this.expiration);else{this.cancel(),i.Log.debug("Timer.init timer "+this._name+" for duration:",e),this._expiration=r;var n=5;e(() => roles_1.Add(role)); + } + + [Fact] + public void Should_do_nothing_if_role_to_add_is_default() + { + var roles_1 = roles_0.Add(Role.Developer); + + Assert.True(roles_1.CustomCount > 0); + } + + [Fact] + public void Should_update_role() + { + var roles_1 = roles_0.Update(firstRole, "P1", "P2"); + + roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2"))); + } + + [Fact] + public void Should_return_same_roles_if_role_not_found() + { + var roles_1 = roles_0.Update(role, "P1", "P2"); + + Assert.Same(roles_0, roles_1); + } + + [Fact] + public void Should_remove_role() + { + var roles_1 = roles_0.Remove(firstRole); + + Assert.Equal(0, roles_1.CustomCount); + } + + [Fact] + public void Should_do_nothing_if_remove_role_not_found() + { + var roles_1 = roles_0.Remove(role); + + Assert.True(roles_1.CustomCount > 0); + } + + [Fact] + public void Should_get_custom_roles() + { + var names = roles_0.Custom.Select(x => x.Name).ToArray(); + + Assert.Equal(new[] { firstRole }, names); + } + + [Fact] + public void Should_get_all_roles() + { + var names = roles_0.All.Select(x => x.Name).ToArray(); + + Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); + } + + [Fact] + public void Should_check_for_custom_role() + { + Assert.True(roles_0.ContainsCustom(firstRole)); + } + + [Fact] + public void Should_check_for_non_custom_role() + { + Assert.False(roles_0.ContainsCustom(Role.Owner)); + } + + [Fact] + public void Should_check_for_default_role() + { + Assert.True(Roles.IsDefault(Role.Owner)); + } + + [Fact] + public void Should_check_for_non_default_role() + { + Assert.False(Roles.IsDefault(firstRole)); + } + + [InlineData("Developer")] + [InlineData("Editor")] + [InlineData("Owner")] + [InlineData("Reader")] + [Theory] + public void Should_get_default_roles(string name) + { + var found = roles_0.TryGet("app", name, out var role); + + Assert.True(found); + Assert.True(role!.IsDefault); + Assert.True(roles_0.Contains(name)); + + foreach (var permission in role.Permissions) + { + Assert.StartsWith("squidex.apps.app.", permission.Id); + } + } + + [Fact] + public void Should_return_null_if_role_not_found() + { + var found = roles_0.TryGet("app", "custom", out var role); + + Assert.False(found); + Assert.Null(role); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs new file mode 100644 index 000000000..3faa18603 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Contents.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class WorkflowJsonTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var workflow = Workflow.Default; + + var serialized = workflow.SerializeAndDeserialize(); + + serialized.Should().BeEquivalentTo(workflow); + } + + [Fact] + public void Should_verify_roles_mapping_in_workflow_transition() + { + var source = new JsonWorkflowTransition { Expression = "expression_1", Role = "role_1" }; + + var serialized = source.SerializeAndDeserialize(); + + var result = serialized.ToTransition(); + + Assert.Equal(new string[] { "role_1" }, result.Roles); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs new file mode 100644 index 000000000..c71529a9a --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs @@ -0,0 +1,147 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Collections; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class WorkflowTests + { + private readonly Workflow workflow = new Workflow( + Status.Draft, new Dictionary + { + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition("ToArchivedExpr", ReadOnlyCollection.Create("ToArchivedRole" )), + [Status.Published] = new WorkflowTransition("ToPublishedExpr", ReadOnlyCollection.Create("ToPublishedRole" )) + }, + StatusColors.Draft), + [Status.Archived] = + new WorkflowStep(), + [Status.Published] = + new WorkflowStep() + }); + + [Fact] + public void Should_provide_default_workflow_if_none_found() + { + var result = Workflows.Empty.GetFirst(); + + Assert.Same(Workflow.Default, result); + } + + [Fact] + public void Should_provide_initial_state() + { + var (status, step) = workflow.GetInitialStep(); + + Assert.Equal(Status.Draft, status); + Assert.Equal(StatusColors.Draft, step.Color); + Assert.Same(workflow.Steps[Status.Draft], step); + } + + [Fact] + public void Should_provide_step() + { + var found = workflow.TryGetStep(Status.Draft, out var step); + + Assert.True(found); + Assert.Same(workflow.Steps[Status.Draft], step); + } + + [Fact] + public void Should_not_provide_unknown_step() + { + var found = workflow.TryGetStep(default, out var step); + + Assert.False(found); + Assert.Null(step); + } + + [Fact] + public void Should_provide_transition() + { + var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition); + + Assert.True(found); + Assert.Equal("ToArchivedExpr", transition!.Expression); + Assert.Equal(new[] { "ToArchivedRole" }, transition!.Roles); + } + + [Fact] + public void Should_provide_transition_to_initial_if_step_not_found() + { + var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition); + + Assert.True(found); + Assert.Null(transition!.Expression); + Assert.Null(transition!.Roles); + } + + [Fact] + public void Should_not_provide_transition_from_unknown_step() + { + var found = workflow.TryGetTransition(new Status("Other"), Status.Archived, out var transition); + + Assert.False(found); + Assert.Null(transition); + } + + [Fact] + public void Should_not_provide_transition_to_unknown_step() + { + var found = workflow.TryGetTransition(Status.Draft, default, out var transition); + + Assert.False(found); + Assert.Null(transition); + } + + [Fact] + public void Should_provide_transitions() + { + var transitions = workflow.GetTransitions(Status.Draft).ToArray(); + + Assert.Equal(2, transitions.Length); + + var (status1, step1, transition1) = transitions[0]; + + Assert.Equal(Status.Archived, status1); + Assert.Equal("ToArchivedExpr", transition1.Expression); + + Assert.Equal(new[] { "ToArchivedRole" }, transition1.Roles); + Assert.Same(workflow.Steps[status1], step1); + + var (status2, step2, transition2) = transitions[1]; + + Assert.Equal(Status.Published, status2); + Assert.Equal("ToPublishedExpr", transition2.Expression); + Assert.Equal(new[] { "ToPublishedRole" }, transition2.Roles); + Assert.Same(workflow.Steps[status2], step2); + } + + [Fact] + public void Should_provide_transitions_to_initial_step_if_status_not_found() + { + var transitions = workflow.GetTransitions(new Status("Other")).ToArray(); + + Assert.Single(transitions); + + var (status1, step1, transition1) = transitions[0]; + + Assert.Equal(Status.Draft, status1); + Assert.Null(transition1.Expression); + Assert.Null(transition1.Roles); + Assert.Same(workflow.Steps[status1], step1); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs new file mode 100644 index 000000000..bd47797b5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model +{ + public class PartitioningTests + { + [Fact] + public void Should_consider_null_as_valid_partitioning() + { + string? partitioning = null; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_invariant_as_valid_partitioning() + { + var partitioning = "invariant"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_language_as_valid_partitioning() + { + var partitioning = "language"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_empty_as_valid_partitioning() + { + var partitioning = string.Empty; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_other_string_as_valid_partitioning() + { + var partitioning = "invalid"; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_provide_invariant_instance() + { + Assert.Equal("invariant", Partitioning.Invariant.Key); + Assert.Equal("invariant", Partitioning.Invariant.ToString()); + } + + [Fact] + public void Should_provide_language_instance() + { + Assert.Equal("language", Partitioning.Language.Key); + Assert.Equal("language", Partitioning.Language.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var partitioning1_a = new Partitioning("partitioning1"); + var partitioning1_b = new Partitioning("partitioning1"); + + var partitioning2 = new Partitioning("partitioning2"); + + Assert.Equal(partitioning1_a, partitioning1_b); + Assert.Equal(partitioning1_a.GetHashCode(), partitioning1_b.GetHashCode()); + Assert.True(partitioning1_a.Equals((object)partitioning1_b)); + + Assert.NotEqual(partitioning1_a, partitioning2); + Assert.NotEqual(partitioning1_a.GetHashCode(), partitioning2.GetHashCode()); + Assert.False(partitioning1_a.Equals((object)partitioning2)); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs new file mode 100644 index 000000000..352a3260d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -0,0 +1,169 @@ +// ========================================================================== +// 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 FluentAssertions; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Core.Model.Rules +{ + public class RuleTests + { + public static readonly List Triggers = + typeof(Rule).Assembly.GetTypes() + .Where(x => x.BaseType == typeof(RuleTrigger)) + .Select(Activator.CreateInstance) + .Select(x => new[] { x }) + .ToList()!; + + private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1()); + + public sealed class OtherTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } + } + + public sealed class MigratedTrigger : RuleTrigger, IMigrated + { + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } + + public RuleTrigger Migrate() + { + return new OtherTrigger(); + } + } + + [TypeName(nameof(TestAction1))] + public sealed class TestAction1 : RuleAction + { + public string Property { get; set; } + } + + [TypeName(nameof(TestAction2))] + public sealed class TestAction2 : RuleAction + { + public string Property { get; set; } + } + + [Fact] + public void Should_create_with_trigger_and_action() + { + var ruleTrigger = new ContentChangedTriggerV2(); + var ruleAction = new TestAction1(); + + var newRule = new Rule(ruleTrigger, ruleAction); + + Assert.Equal(ruleTrigger, newRule.Trigger); + Assert.Equal(ruleAction, newRule.Action); + Assert.True(newRule.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_true_when_enabling() + { + var rule_1 = rule_0.Disable(); + var rule_2 = rule_1.Enable(); + var rule_3 = rule_2.Enable(); + + Assert.False(rule_1.IsEnabled); + Assert.True(rule_3.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_false_when_disabling() + { + var rule_1 = rule_0.Disable(); + var rule_2 = rule_1.Disable(); + + Assert.True(rule_0.IsEnabled); + Assert.False(rule_2.IsEnabled); + } + + [Fact] + public void Should_replace_name_when_renaming() + { + var rule_1 = rule_0.Rename("MyName"); + + Assert.Equal("MyName", rule_1.Name); + } + + [Fact] + public void Should_replace_trigger_when_updating() + { + var newTrigger = new ContentChangedTriggerV2(); + + var rule_1 = rule_0.Update(newTrigger); + + Assert.NotSame(newTrigger, rule_0.Trigger); + Assert.Same(newTrigger, rule_1.Trigger); + } + + [Fact] + public void Should_throw_exception_when_new_trigger_has_other_type() + { + Assert.Throws(() => rule_0.Update(new OtherTrigger())); + } + + [Fact] + public void Should_replace_action_when_updating() + { + var newAction = new TestAction1(); + + var rule_1 = rule_0.Update(newAction); + + Assert.NotSame(newAction, rule_0.Action); + Assert.Same(newAction, rule_1.Action); + } + + [Fact] + public void Should_throw_exception_when_new_action_has_other_type() + { + Assert.Throws(() => rule_0.Update(new TestAction2())); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var rule_1 = rule_0.Disable(); + + var serialized = rule_1.SerializeAndDeserialize(); + + serialized.Should().BeEquivalentTo(rule_1); + } + + [Fact] + public void Should_serialize_and_deserialize_and_migrate_trigger() + { + var rule_X = new Rule(new MigratedTrigger(), new TestAction1()); + + var serialized = rule_X.SerializeAndDeserialize(); + + Assert.IsType(serialized.Trigger); + } + + [Theory] + [MemberData(nameof(Triggers))] + public void Should_freeze_triggers(RuleTrigger trigger) + { + TestUtils.TestFreeze(trigger); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs new file mode 100644 index 000000000..2ac4614a4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// 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.Domain.Apps.Core.Schemas; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class SchemaFieldTests + { + public static readonly List FieldProperties = + typeof(Schema).Assembly.GetTypes() + .Where(x => x.BaseType == typeof(FieldProperties)) + .Select(Activator.CreateInstance) + .Select(x => new[] { x }) + .ToList()!; + + private readonly RootField field_0 = Fields.Number(1, "my-field", Partitioning.Invariant); + + [Fact] + public void Should_instantiate_field() + { + Assert.True(field_0.RawProperties.IsFrozen); + Assert.Equal("my-field", field_0.Name); + } + + [Fact] + public void Should_throw_exception_if_creating_field_with_invalid_name() + { + Assert.Throws(() => Fields.Number(1, string.Empty, Partitioning.Invariant)); + } + + [Fact] + public void Should_hide_field() + { + var field_1 = field_0.Hide(); + var field_2 = field_1.Hide(); + + Assert.False(field_0.IsHidden); + Assert.True(field_2.IsHidden); + } + + [Fact] + public void Should_show_field() + { + var field_1 = field_0.Hide(); + var field_2 = field_1.Show(); + var field_3 = field_2.Show(); + + Assert.True(field_1.IsHidden); + Assert.False(field_3.IsHidden); + } + + [Fact] + public void Should_disable_field() + { + var field_1 = field_0.Disable(); + var field_2 = field_1.Disable(); + + Assert.False(field_0.IsDisabled); + Assert.True(field_2.IsDisabled); + } + + [Fact] + public void Should_enable_field() + { + var field_1 = field_0.Disable(); + var field_2 = field_1.Enable(); + var field_3 = field_2.Enable(); + + Assert.True(field_1.IsDisabled); + Assert.False(field_3.IsDisabled); + } + + [Fact] + public void Should_lock_field() + { + var field_1 = field_0.Lock(); + + Assert.False(field_0.IsLocked); + Assert.True(field_1.IsLocked); + } + + [Fact] + public void Should_update_field() + { + var field_1 = field_0.Update(new NumberFieldProperties { Hints = "my-hints" }); + + Assert.Null(field_0.RawProperties.Hints); + Assert.True(field_1.RawProperties.IsFrozen); + Assert.Equal("my-hints", field_1.RawProperties.Hints); + } + + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + Assert.Throws(() => field_0.Update(new StringFieldProperties())); + } + + [Theory] + [MemberData(nameof(FieldProperties))] + public void Should_freeze_field_properties(FieldProperties action) + { + TestUtils.TestFreeze(action); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs new file mode 100644 index 000000000..bd7eea917 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +{ + public class ContentConversionFlatTests + { + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + [Fact] + public void Should_return_original_when_no_language_preferences_defined() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)); + + Assert.Same(data, data.ToFlatLanguageModel(languagesConfig)); + } + + [Fact] + public void Should_return_flatten_value() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var output = data.ToFlatten(); + + var expected = new Dictionary + { + { + "field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2) + }, + { + "field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4) + }, + { "field3", JsonValue.Create(6) }, + { "field4", JsonValue.Create(7) } + }; + + Assert.True(expected.EqualsDictionary(output)); + } + + [Fact] + public void Should_return_flat_list_when_single_languages_specified() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var fallbackConfig = + LanguagesConfig.Build( + new LanguageConfig(Language.EN), + new LanguageConfig(Language.DE, false, Language.EN)); + + var output = (Dictionary)data.ToFlatLanguageModel(fallbackConfig, new List { Language.DE }); + + var expected = new Dictionary + { + { "field1", JsonValue.Create(1) }, + { "field2", JsonValue.Create(4) }, + { "field3", JsonValue.Create(6) } + }; + + Assert.True(expected.EqualsDictionary(output)); + } + + [Fact] + public void Should_return_flat_list_when_languages_specified() + { + var data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var output = (Dictionary)data.ToFlatLanguageModel(languagesConfig, new List { Language.DE, Language.EN }); + + var expected = new Dictionary + { + { "field1", JsonValue.Create(1) }, + { "field2", JsonValue.Create(4) }, + { "field3", JsonValue.Create(6) } + }; + + Assert.True(expected.EqualsDictionary(output)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs new file mode 100644 index 000000000..6ee85e7a5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions + +namespace Squidex.Domain.Apps.Core.Operations.EnrichContent +{ + public class ContentEnrichmentTests + { + private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + private readonly Schema schema; + + public ContentEnrichmentTests() + { + schema = + new Schema("my-schema") + .AddString(1, "my-string", Partitioning.Language, + new StringFieldProperties { DefaultValue = "en-string" }) + .AddNumber(2, "my-number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddDateTime(3, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = now }) + .AddBoolean(4, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); + } + + [Fact] + public void Should_enrich_with_default_values() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "de-string")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal(456, ((JsonScalar)data["my-number"]!["iv"]).Value); + + Assert.Equal("de-string", data["my-string"]!["de"].ToString()); + Assert.Equal("en-string", data["my-string"]!["en"].ToString()); + + Assert.Equal(now.ToString(), data["my-datetime"]!["iv"].ToString()); + + Assert.True(((JsonScalar)data["my-boolean"]!["iv"]).Value); + } + + [Fact] + public void Should_also_enrich_with_default_values_when_string_is_empty() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", string.Empty)) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal("en-string", data["my-string"]!["de"].ToString()); + Assert.Equal("en-string", data["my-string"]!["en"].ToString()); + } + + [Fact] + public void Should_get_default_value_from_assets_field() + { + var field = + Fields.Assets(1, "1", Partitioning.Invariant, + new AssetsFieldProperties()); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_boolean_field() + { + var field = + Fields.Boolean(1, "1", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); + + Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); + + Assert.Equal(JsonValue.Create(FutureDays(15).ToString()), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_today() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); + + Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_now() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); + + Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_json_field() + { + var field = + Fields.Json(1, "1", Partitioning.Invariant, + new JsonFieldProperties()); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_geolocation_field() + { + var field = + Fields.Geolocation(1, "1", Partitioning.Invariant, + new GeolocationFieldProperties()); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_number_field() + { + var field = + Fields.Number(1, "1", Partitioning.Invariant, + new NumberFieldProperties { DefaultValue = 12 }); + + Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_references_field() + { + var field = + Fields.References(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties()); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_string_field() + { + var field = + Fields.String(1, "1", Partitioning.Invariant, + new StringFieldProperties { DefaultValue = "default" }); + + Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + [Fact] + public void Should_get_default_value_from_tags_field() + { + var field = + Fields.Tags(1, "1", Partitioning.Invariant, + new TagsFieldProperties()); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + } + + private Instant FutureDays(int days) + { + return now.WithoutMs().Plus(Duration.FromDays(days)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs new file mode 100644 index 000000000..491fdf39d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -0,0 +1,606 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.EventSynchronization; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization +{ + public class SchemaSynchronizerTests + { + private readonly Func idGenerator; + private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer; + private readonly NamedId stringId = NamedId.Of(13L, "my-value"); + private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); + private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); + private int fields = 50; + + public SchemaSynchronizerTests() + { + idGenerator = () => fields++; + } + + [Fact] + public void Should_create_events_if_schema_deleted() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + (Schema?)null; + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaDeleted() + ); + } + + [Fact] + public void Should_create_events_if_category_changed() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .ChangeCategory("Category"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaCategoryChanged { Name = "Category" } + ); + } + + [Fact] + public void Should_create_events_if_scripts_configured() + { + var scripts = new SchemaScripts + { + Create = "" + }; + + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target").ConfigureScripts(scripts); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaScriptsConfigured { Scripts = scripts } + ); + } + + [Fact] + public void Should_create_events_if_preview_urls_configured() + { + var previewUrls = new Dictionary + { + ["web"] = "Url" + }; + + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .ConfigurePreviewUrls(previewUrls); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } + ); + } + + [Fact] + public void Should_create_events_if_schema_published() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .Publish(); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaPublished() + ); + } + + [Fact] + public void Should_create_events_if_schema_unpublished() + { + var sourceSchema = + new Schema("source") + .Publish(); + + var targetSchema = + new Schema("target"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaUnpublished() + ); + } + + [Fact] + public void Should_create_events_if_nested_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target"); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; + + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name, properties)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; + + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_locked() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .LockField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_locked() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .LockField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_shown() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_shown() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .DisableField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .DisableField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .DisableField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .DisableField(stringId.Id); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_field_created() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = createdId } + ); + } + + [Fact] + public void Should_create_events_if_field_type_has_changed() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId }, + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() } + ); + } + + [Fact] + public void Should_create_events_if_field_partitioning_has_changed() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Language); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var createdId = NamedId.Of(50L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId }, + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Language.Key, Properties = new StringFieldProperties() } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_created() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + var id1 = NamedId.Of(50L, arrayId.Name); + var id2 = NamedId.Of(51L, stringId.Name); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, + new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = id2, ParentFieldId = id1 } + ); + } + + [Fact] + public void Should_create_events_if_nested_fields_reordered() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(10, "f1") + .AddString(11, "f2")); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(1, "f2") + .AddString(2, "f1")); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new List { 11, 10 }, ParentFieldId = arrayId } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f2", Partitioning.Invariant) + .AddString(2, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered_after_sync() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f3", Partitioning.Invariant) + .AddString(2, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new List { 50, 10 } } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered_after_sync2() + { + var id1 = NamedId.Of(1, "f1"); + var id2 = NamedId.Of(2, "f1"); + + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f1", Partitioning.Invariant) + .AddString(2, "f3", Partitioning.Invariant) + .AddString(3, "f2", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new List { 10, 50, 11 } } + ); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs new file mode 100644 index 000000000..f4282c76c --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -0,0 +1,308 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +{ + public class ReferenceExtractionTests + { + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Schema schema; + + public ReferenceExtractionTests() + { + schema = + new Schema("my-schema") + .AddNumber(1, "field1", Partitioning.Language) + .AddNumber(2, "field2", Partitioning.Invariant) + .AddNumber(3, "field3", Partitioning.Invariant) + .AddAssets(5, "assets1", Partitioning.Invariant) + .AddAssets(6, "assets2", Partitioning.Invariant) + .AddArray(7, "array", Partitioning.Invariant, a => a + .AddAssets(71, "assets71")) + .AddJson(4, "json", Partitioning.Language) + .UpdateField(3, f => f.Hide()); + } + + [Fact] + public void Should_get_ids_from_id_data() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); + + var ids = input.GetReferencedIds(schema).ToArray(); + + Assert.Equal(new[] { id1, id2 }, ids); + } + + [Fact] + public void Should_get_ids_from_name_data() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new NamedContentData() + .AddField("assets1", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); + + var ids = input.GetReferencedIds(schema).ToArray(); + + Assert.Equal(new[] { id1, id2 }, ids); + } + + [Fact] + public void Should_cleanup_deleted_ids() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); + + var converter = FieldConverters.ForValues(ValueReferencesConverter.CleanReferences(new[] { id2 })); + + var actual = input.ConvertId2Id(schema, converter); + + var cleanedValue = (JsonArray)actual[5]!["iv"]; + + Assert.Equal(1, cleanedValue.Count); + Assert.Equal(id1.ToString(), cleanedValue[0].ToString()); + } + + [Fact] + public void Should_return_ids_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2 }, result); + } + + [Fact] + public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_null() + { + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.GetReferencedIds(null).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_other_type() + { + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_empty_list_from_non_references_field() + { + var sut = Fields.String(1, "my-string", Partitioning.Invariant); + + var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_null_from_assets_field_when_removing_references_from_null_array() + { + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(JsonValue.Null, null); + + Assert.Equal(JsonValue.Null, result); + } + + [Fact] + public void Should_remove_deleted_references_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); + + Assert.Equal(CreateValue(id1), result); + } + + [Fact] + public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); + + var token = CreateValue(id1, id2); + + var result = sut.CleanReferences(token, HashSet.Of(Guid.NewGuid())); + + Assert.Same(token, result); + } + + [Fact] + public void Should_return_ids_from_nested_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = + Fields.Array(1, "my-array", Partitioning.Invariant, + Fields.References(1, "my-refs", + new ReferencesFieldProperties { SchemaId = schemaId })); + + var value = + JsonValue.Array( + JsonValue.Object() + .Add("my-refs", CreateValue(id1, id2))); + + var result = sut.GetReferencedIds(value).ToArray(); + + Assert.Equal(new[] { id1, id2, schemaId }, result); + } + + [Fact] + public void Should_return_ids_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2, schemaId }, result); + } + + [Fact] + public void Should_return_ids_from_references_field_without_schema_id() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(CreateValue(id1, id2), Ids.ContentOnly).ToArray(); + + Assert.Equal(new[] { id1, id2 }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() + { + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(JsonValue.Null).ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() + { + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_null_from_references_field_when_removing_references_from_null_array() + { + var sut = Fields.References(1, "my-refs", Partitioning.Invariant); + + var result = sut.CleanReferences(JsonValue.Null, null); + + Assert.Equal(JsonValue.Null, result); + } + + [Fact] + public void Should_remove_deleted_references_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); + + Assert.Equal(CreateValue(id1), result); + } + + [Fact] + public void Should_remove_all_references_from_references_field_when_schema_is_removed() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(schemaId)); + + Assert.Equal(CreateValue(), result); + } + + [Fact] + public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = Fields.References(1, "my-refs", Partitioning.Invariant); + + var value = CreateValue(id1, id2); + + var result = sut.CleanReferences(value, HashSet.Of(Guid.NewGuid())); + + Assert.Same(value, result); + } + + private static IJsonValue CreateValue(params Guid[] ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs new file mode 100644 index 000000000..630429be8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -0,0 +1,331 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; +using Xunit; + +#pragma warning disable xUnit2009 // Do not use boolean check to check for string equality + +namespace Squidex.Domain.Apps.Core.Operations.HandleRules +{ + public class RuleServiceTests + { + private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake(); + private readonly IRuleActionHandler ruleActionHandler = A.Fake(); + private readonly IEventEnricher eventEnricher = A.Fake(); + private readonly IClock clock = A.Fake(); + private readonly string actionData = "{\"value\":10}"; + private readonly string actionDump = "MyDump"; + private readonly string actionName = "ValidAction"; + private readonly string actionDescription = "MyDescription"; + private readonly Guid ruleId = Guid.NewGuid(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + private readonly RuleService sut; + + public sealed class InvalidEvent : IEvent + { + } + + public sealed class InvalidAction : RuleAction + { + } + + public sealed class ValidAction : RuleAction + { + } + + public sealed class ValidData + { + public int Value { get; set; } + } + + public sealed class InvalidTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + return default!; + } + } + + public RuleServiceTests() + { + typeNameRegistry.Map(typeof(ContentCreated)); + typeNameRegistry.Map(typeof(ValidAction), actionName); + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + + A.CallTo(() => ruleActionHandler.ActionType) + .Returns(typeof(ValidAction)); + + A.CallTo(() => ruleActionHandler.DataType) + .Returns(typeof(ValidData)); + + A.CallTo(() => ruleTriggerHandler.TriggerType) + .Returns(typeof(ContentChangedTriggerV2)); + + var log = A.Fake(); + + sut = new RuleService(Options.Create(new RuleOptions()), + new[] { ruleTriggerHandler }, + new[] { ruleActionHandler }, + eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry); + } + + [Fact] + public async Task Should_not_create_job_if_rule_disabled() + { + var @event = Envelope.Create(new ContentCreated()); + + var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_for_invalid_event() + { + var @event = Envelope.Create(new InvalidEvent()); + + var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_no_trigger_handler_registered() + { + var @event = Envelope.Create(new ContentCreated()); + + var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_no_action_handler_registered() + { + var @event = Envelope.Create(new ContentCreated()); + + var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_too_old() + { + var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + + var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_not_triggered_with_precheck() + { + var rule = ValidRule(); + + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(false); + + var job = await sut.CreateJobAsync(rule, ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_enriched_event_not_created() + { + var rule = ValidRule(); + + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(Task.FromResult(null)); + + var job = await sut.CreateJobAsync(rule, ruleId, @event); + + Assert.Null(job); + } + + [Fact] + public async Task Should_not_create_job_if_not_triggered() + { + var rule = ValidRule(); + + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); + + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) + .Returns(false); + + var job = await sut.CreateJobAsync(rule, ruleId, @event); + + Assert.Null(job); + } + + [Fact] + public async Task Should_create_job_if_triggered() + { + var now = clock.GetCurrentInstant(); + + var rule = ValidRule(); + + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + + var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); + + A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) + .Returns((actionDescription, new ValidData { Value = 10 })); + + var job = (await sut.CreateJobAsync(rule, ruleId, @event))!; + + Assert.Equal(actionData, job.ActionData); + Assert.Equal(actionName, job.ActionName); + Assert.Equal(actionDescription, job.Description); + + Assert.Equal(now, job.Created); + Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires); + + Assert.Equal(enrichedEvent.AppId.Id, job.AppId); + + Assert.NotEqual(Guid.Empty, job.Id); + + A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A>.That.Matches(x => x.Payload == @event.Payload))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Returns(Result.Success(actionDump)); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(RuleResult.Success, result.Result.Status); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Returns(Result.Failed(new InvalidOperationException(), actionDump)); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(RuleResult.Failed, result.Result.Status); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Returns(Result.Failed(new TimeoutException(), actionDump)); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(RuleResult.Timeout, result.Result.Status); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + + Assert.True(result.Result.Dump?.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); + } + + [Fact] + public async Task Should_create_exception_details_when_job_to_execute_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) + .Throws(ex); + + var result = await sut.InvokeAsync(actionName, actionData); + + Assert.Equal(ex, result.Result.Exception); + } + + private static Rule RuleInvalidAction() + { + return new Rule(new ContentChangedTriggerV2(), new InvalidAction()); + } + + private static Rule RuleInvalidTrigger() + { + return new Rule(new InvalidTrigger(), new ValidAction()); + } + + private static Rule ValidRule() + { + return new Rule(new ContentChangedTriggerV2(), new ValidAction()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs new file mode 100644 index 000000000..86d1690c8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs @@ -0,0 +1,134 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.Tags +{ + public class TagNormalizerTests + { + private readonly ITagService tagService = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Schema schema; + + public TagNormalizerTests() + { + schema = + new Schema("my-schema") + .AddTags(1, "tags1", Partitioning.Invariant) + .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) + .AddString(3, "string", Partitioning.Invariant) + .AddArray(4, "array", Partitioning.Invariant, f => f + .AddTags(401, "nestedTags1") + .AddTags(402, "nestedTags2", new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) + .AddString(403, "string")); + } + + [Fact] + public async Task Should_normalize_tags_with_old_data() + { + var newData = GenerateData("n_raw"); + var oldData = GenerateData("o_raw"); + + A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), + A>.That.IsSameSequenceAs("n_raw2_1", "n_raw2_2", "n_raw4"), + A>.That.IsSameSequenceAs("o_raw2_1", "o_raw2_2", "o_raw4"))) + .Returns(new Dictionary + { + ["n_raw2_2"] = "id2_2", + ["n_raw2_1"] = "id2_1", + ["n_raw4"] = "id4" + }); + + await tagService.NormalizeAsync(appId, schemaId, schema, newData, oldData); + + Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); + Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); + } + + [Fact] + public async Task Should_normalize_tags_without_old_data() + { + var newData = GenerateData("name"); + + A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), + A>.That.IsSameSequenceAs("name2_1", "name2_2", "name4"), + A>.That.IsEmpty())) + .Returns(new Dictionary + { + ["name2_2"] = "id2_2", + ["name2_1"] = "id2_1", + ["name4"] = "id4" + }); + + await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); + + Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); + Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); + } + + [Fact] + public async Task Should_denormalize_tags() + { + var newData = GenerateData("id"); + + A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), + A>.That.IsSameSequenceAs("id2_1", "id2_2", "id4"), + A>.That.IsEmpty())) + .Returns(new Dictionary + { + ["id2_2"] = "name2_2", + ["id2_1"] = "name2_1", + ["id4"] = "name4" + }); + + await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); + + Assert.Equal(JsonValue.Array("name2_1", "name2_2"), newData["tags2"]!["iv"]); + Assert.Equal(JsonValue.Array("name4"), GetNestedTags(newData)); + } + + private static IJsonValue GetNestedTags(NamedContentData newData) + { + var array = (JsonArray)newData["array"]!["iv"]; + var arrayItem = (JsonObject)array[0]; + + return arrayItem["nestedTags2"]; + } + + private static NamedContentData GenerateData(string prefix) + { + return new NamedContentData() + .AddField("tags1", + new ContentFieldData() + .AddValue("iv", JsonValue.Array($"{prefix}1"))) + .AddField("tags2", + new ContentFieldData() + .AddValue("iv", JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) + .AddField("string", + new ContentFieldData() + .AddValue("iv", $"{prefix}stringValue")) + .AddField("array", + new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("nestedTags1", JsonValue.Array($"{prefix}3")) + .Add("nestedTags2", JsonValue.Array($"{prefix}4")) + .Add("string", $"{prefix}nestedStringValue")))); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs new file mode 100644 index 000000000..fa5a34669 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs @@ -0,0 +1,125 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class ArrayFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ArrayFieldProperties()); + + Assert.Equal("my-array", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_items_are_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_items_are_null_and_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_items_is_equal_to_min_and_max_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_items_are_required_and_null() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_items_are_required_and_empty() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new ArrayFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + private static IJsonValue CreateValue(params JsonObject[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); + } + + private static RootField Field(ArrayFieldProperties properties) + { + return Fields.Array(1, "my-array", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs new file mode 100644 index 000000000..4e05cdcee --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -0,0 +1,321 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class AssetsFieldTests + { + private readonly List errors = new List(); + + public sealed class AssetInfo : IAssetInfo + { + public Guid AssetId { get; set; } + + public string FileName { get; set; } + + public string FileHash { get; set; } + + public string Slug { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + } + + private readonly AssetInfo document = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyDocument.pdf", + FileSize = 1024 * 4, + IsImage = false, + PixelWidth = null, + PixelHeight = null + }; + + private readonly AssetInfo image1 = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyImage.png", + FileSize = 1024 * 8, + IsImage = true, + PixelWidth = 800, + PixelHeight = 600 + }; + + private readonly AssetInfo image2 = new AssetInfo + { + AssetId = Guid.NewGuid(), + FileName = "MyImage.png", + FileSize = 1024 * 8, + IsImage = true, + PixelWidth = 800, + PixelHeight = 600 + }; + + private readonly ValidationContext ctx; + + public AssetsFieldTests() + { + ctx = ValidationTestExtensions.Assets(image1, image2, document); + } + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new AssetsFieldProperties()); + + Assert.Equal("my-assets", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_assets_are_valid() + { + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(document.AssetId), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_assets_are_null_and_valid() + { + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() + { + var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_duplicate_values_are_ignored() + { + var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_assets_are_required_and_null() + { + var sut = Field(new AssetsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_assets_are_required_and_empty() + { + var sut = Field(new AssetsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new AssetsFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_asset_are_not_valid() + { + var assetId = Guid.NewGuid(); + + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(assetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { $"[1]: Id '{assetId}' not found." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_too_small() + { + var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_too_big() + { + var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." }); + } + + [Fact] + public async Task Should_add_error_if_document_is_not_an_image() + { + var sut = Field(new AssetsFieldProperties { MustBeImage = true }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Not an image." }); + } + + [Fact] + public async Task Should_add_error_if_values_contains_duplicate() + { + var sut = Field(new AssetsFieldProperties { MustBeImage = true }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } + + [Fact] + public async Task Should_add_error_if_image_width_is_too_small() + { + var sut = Field(new AssetsFieldProperties { MinWidth = 1000 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_width_is_too_big() + { + var sut = Field(new AssetsFieldProperties { MaxWidth = 700 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_height_is_too_small() + { + var sut = Field(new AssetsFieldProperties { MinHeight = 800 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_height_is_too_big() + { + var sut = Field(new AssetsFieldProperties { MaxHeight = 500 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." }); + } + + [Fact] + public async Task Should_add_error_if_image_has_invalid_aspect_ratio() + { + var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] { "[2]: Aspect ratio not '1:1'." }); + } + + [Fact] + public async Task Should_add_error_if_image_has_invalid_extension() + { + var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") }); + + await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); + + errors.Should().BeEquivalentTo( + new[] + { + "[1]: Invalid file extension.", + "[2]: Invalid file extension." + }); + } + + private static IJsonValue CreateValue(params Guid[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } + + private static RootField Field(AssetsFieldProperties properties) + { + return Fields.Assets(1, "my-assets", Partitioning.Invariant, properties); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs new file mode 100644 index 000000000..9988da835 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -0,0 +1,192 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class ReferencesFieldTests + { + private readonly List errors = new List(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid ref1 = Guid.NewGuid(); + private readonly Guid ref2 = Guid.NewGuid(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ReferencesFieldProperties()); + + Assert.Equal("my-refs", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_references_are_valid() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(ref1), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_references_are_null_and_valid() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() + { + var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_duplicate_values_are_allowed() + { + var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); + + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_not_defined() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_references_are_required_and_null() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_references_are_required_and_empty() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_reference_are_not_valid() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References()); + + errors.Should().BeEquivalentTo( + new[] { $"Contains invalid reference '{ref1}'." }); + } + + [Fact] + public async Task Should_add_error_if_reference_schema_is_not_valid() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); + + errors.Should().BeEquivalentTo( + new[] { $"Contains reference '{ref1}' to invalid schema." }); + } + + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, + ValidationTestExtensions.References( + (schemaId, ref1))); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } + + private static IJsonValue CreateValue(params Guid[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } + + private ValidationContext Context() + { + return ValidationTestExtensions.References( + (schemaId, ref1), + (schemaId, ref2)); + } + + private static RootField Field(ReferencesFieldProperties properties) + { + return Fields.References(1, "my-refs", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs new file mode 100644 index 000000000..d05115cb8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class StringFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new StringFieldProperties()); + + Assert.Equal("my-string", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_string_is_valid() + { + var sut = Field(new StringFieldProperties { Label = "" }); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_string_is_required_but_null() + { + var sut = Field(new StringFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_required_but_empty() + { + var sut = Field(new StringFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(string.Empty), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_shorter_than_min_length() + { + var sut = Field(new StringFieldProperties { MinLength = 10 }); + + await sut.ValidateAsync(CreateValue("123"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 10 character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_longer_than_max_length() + { + var sut = Field(new StringFieldProperties { MaxLength = 5 }); + + await sut.ValidateAsync(CreateValue("12345678"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 5 character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_string_not_allowed() + { + var sut = Field(new StringFieldProperties { AllowedValues = ReadOnlyCollection.Create("Foo") }); + + await sut.ValidateAsync(CreateValue("Bar"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not an allowed value." }); + } + + [Fact] + public async Task Should_add_error_if_number_is_not_valid_pattern() + { + var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); + + await sut.ValidateAsync(CreateValue("abc"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Does not match to the pattern." }); + } + + [Fact] + public async Task Should_add_error_if_number_is_not_valid_pattern_with_message() + { + var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); + + await sut.ValidateAsync(CreateValue("abc"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Custom Error Message." }); + } + + [Fact] + public async Task Should_add_error_if_unique_constraint_failed() + { + var sut = Field(new StringFieldProperties { IsUnique = true }); + + await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); + + errors.Should().BeEquivalentTo( + new[] { "Another content with the same value exists." }); + } + + private static IJsonValue CreateValue(string? v) + { + return JsonValue.Create(v); + } + + private static RootField Field(StringFieldProperties properties) + { + return Fields.String(1, "my-string", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs new file mode 100644 index 000000000..d69d1993d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class TagsFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new TagsFieldProperties()); + + Assert.Equal("my-tags", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_tags_are_valid() + { + var sut = Field(new TagsFieldProperties()); + + await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_tags_are_null_and_valid() + { + var sut = Field(new TagsFieldProperties()); + + await sut.ValidateAsync(CreateValue(null), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_number_of_tags_is_equal_to_min_and_max_items() + { + var sut = Field(new TagsFieldProperties { MinItems = 2, MaxItems = 2 }); + + await sut.ValidateAsync(CreateValue("tag1", "tag2"), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_tags_are_required_but_null() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(null), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_tags_are_required_but_empty() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_tag_value_is_null() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(JsonValue.Array(JsonValue.Null), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_tag_value_is_empty() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(string.Empty), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new TagsFieldProperties()); + + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Not a valid value." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new TagsFieldProperties { MinItems = 3 }); + + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new TagsFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_contains_an_not_allowed_values() + { + var sut = Field(new TagsFieldProperties { AllowedValues = ReadOnlyCollection.Create("tag-2", "tag-3") }); + + await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors); + + errors.Should().BeEquivalentTo( + new[] { "[1]: Not an allowed value." }); + } + + private static IJsonValue CreateValue(params string?[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); + } + + private static RootField Field(TagsFieldProperties properties) + { + return Fields.Tags(1, "my-tags", Partitioning.Invariant, properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs new file mode 100644 index 000000000..2977b6acb --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public class UIFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new UIFieldProperties()); + + Assert.Equal("my-ui", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_value_is_undefined() + { + var sut = Field(new UIFieldProperties()); + + await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_value_is_json_null() + { + var sut = Field(new UIFieldProperties()); + + await sut.ValidateAsync(JsonValue.Null, errors); + + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_valid() + { + var sut = Field(new UIFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(JsonValue.True, errors); + + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } + + [Fact] + public async Task Should_add_error_if_field_object_is_defined() + { + var schema = + new Schema("my-schema") + .AddUI(1, "my-ui1", Partitioning.Invariant) + .AddUI(2, "my-ui2", Partitioning.Invariant); + + var data = + new NamedContentData() + .AddField("my-ui1", new ContentFieldData()) + .AddField("my-ui2", new ContentFieldData() + .AddValue("iv", null)); + + var validationContext = ValidationTestExtensions.ValidContext; + var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] + { + new ValidationError("Value must not be defined.", "my-ui1"), + new ValidationError("Value must not be defined.", "my-ui2") + }); + } + + [Fact] + public async Task Should_add_error_if_array_item_field_is_defined() + { + var schema = + new Schema("my-schema") + .AddArray(1, "my-array", Partitioning.Invariant, array => array + .AddUI(101, "my-ui")); + + var data = + new NamedContentData() + .AddField("my-array", new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("my-ui", null)))); + + var validationContext = + new ValidationContext( + Guid.NewGuid(), + Guid.NewGuid(), + (c, s) => null!, + (s) => null!, + (c) => null!); + + var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); + } + + private static NestedField Field(UIFieldProperties properties) + { + return new NestedField(1, "my-ui", properties); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs new file mode 100644 index 000000000..ecd4a74dd --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +{ + public static class ValidationTestExtensions + { + private static readonly Task> EmptyReferences = Task.FromResult>(new List<(Guid SchemaId, Guid Id)>()); + private static readonly Task> EmptyAssets = Task.FromResult>(new List()); + + public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), + (x, y) => EmptyReferences, + (x) => EmptyReferences, + (x) => EmptyAssets); + + public static Task ValidateAsync(this IValidator validator, object? value, IList errors, ValidationContext? context = null) + { + return validator.ValidateAsync(value, + CreateContext(context), + CreateFormatter(errors)); + } + + public static Task ValidateOptionalAsync(this IValidator validator, object? value, IList errors, ValidationContext? context = null) + { + return validator.ValidateAsync( + value, + CreateContext(context).Optional(true), + CreateFormatter(errors)); + } + + public static Task ValidateAsync(this IField field, object? value, IList errors, ValidationContext? context = null) + { + return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) + .ValidateAsync( + value, + CreateContext(context), + CreateFormatter(errors)); + } + + private static AddError CreateFormatter(IList errors) + { + return (field, message) => + { + if (field == null || !field.Any()) + { + errors.Add(message); + } + else + { + errors.Add($"{field.ToPathString()}: {message}"); + } + }; + } + + private static ValidationContext CreateContext(ValidationContext? context) + { + return context ?? ValidContext; + } + + public static ValidationContext Assets(params IAssetInfo[] assets) + { + var actual = Task.FromResult>(assets.ToList()); + + return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); + } + + public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) + { + var actual = Task.FromResult>(referencesIds.ToList()); + + return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj new file mode 100644 index 000000000..a0012f39f --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -0,0 +1,33 @@ + + + Exe + netcoreapp3.0 + Squidex.Domain.Apps.Core + 8.0 + enable + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs new file mode 100644 index 000000000..aa535de60 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs @@ -0,0 +1,173 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Contents.Json; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; +using Xunit; + +namespace Squidex.Domain.Apps.Core +{ + public static class TestUtils + { + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + + public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto) + { + var typeNameRegistry = + new TypeNameRegistry() + .Map(new FieldRegistry()) + .Map(new RuleRegistry()) + .MapUnmapped(typeof(TestUtils).Assembly); + + var serializerSettings = new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), + + ContractResolver = new ConverterContractResolver( + new AppClientsConverter(), + new AppContributorsConverter(), + new AppPatternsConverter(), + new ClaimsPrincipalConverter(), + new ContentFieldDataConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new InstantConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new LanguagesConfigConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new RolesConverter(), + new RuleConverter(), + new SchemaConverter(), + new StatusConverter(), + new StringEnumConverter(), + new WorkflowConverter(), + new WorkflowTransitionConverter()), + + TypeNameHandling = typeNameHandling + }; + + return new NewtonsoftJsonSerializer(serializerSettings); + } + + public static Schema MixedSchema(bool isSingleton = false) + { + var schema = new Schema("user", isSingleton: isSingleton) + .Publish() + .AddArray(101, "root-array", Partitioning.Language, f => f + .AddAssets(201, "nested-assets") + .AddBoolean(202, "nested-boolean") + .AddDateTime(203, "nested-datetime") + .AddGeolocation(204, "nested-geolocation") + .AddJson(205, "nested-json") + .AddJson(211, "nested-json2") + .AddNumber(206, "nested-number") + .AddReferences(207, "nested-references") + .AddString(208, "nested-string") + .AddTags(209, "nested-tags") + .AddUI(210, "nested-ui")) + .AddAssets(102, "root-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(103, "root-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(104, "root-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) + .AddDateTime(105, "root-date", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) + .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddJson(107, "root-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddNumber(108, "root-number", Partitioning.Invariant, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) + .AddReferences(109, "root-references", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(110, "root-string1", Partitioning.Invariant, + new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadOnlyCollection.Create("a", "b") }) + .AddString(111, "root-string2", Partitioning.Invariant, + new StringFieldProperties { Hints = "My String1" }) + .AddTags(112, "root-tags", Partitioning.Language, + new TagsFieldProperties()) + .AddUI(113, "root-ui", Partitioning.Language, + new UIFieldProperties()) + .Update(new SchemaProperties { Hints = "The User" }) + .HideField(104) + .HideField(211, 101) + .DisableField(109) + .DisableField(212, 101) + .LockField(105); + + return schema; + } + + public static T SerializeAndDeserialize(this T value) + { + return DefaultSerializer.Deserialize(DefaultSerializer.Serialize(value)); + } + + public static void TestFreeze(IFreezable sut) + { + var properties = + sut.GetType().GetRuntimeProperties() + .Where(x => + x.CanWrite && + x.CanRead && + x.Name != "IsFrozen"); + + foreach (var property in properties) + { + var value = + property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; + + property.SetValue(sut, value); + + var result = property.GetValue(sut); + + Assert.Equal(value, result); + } + + sut.Freeze(); + + foreach (var property in properties) + { + var value = + property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; + + Assert.Throws(() => + { + try + { + property.SetValue(sut, value); + } + catch (Exception ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + }); + } + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs new file mode 100644 index 000000000..977e0fde9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable IDE0067 // Dispose objects before losing scope + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppCommandMiddlewareTests : HandlerTestBase + { + private readonly IContextProvider contextProvider = A.Fake(); + private readonly IAssetStore assetStore = A.Fake(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Context requestContext = Context.Anonymous(); + private readonly AppCommandMiddleware sut; + + public sealed class MyCommand : SquidexCommand + { + } + + protected override Guid Id + { + get { return appId; } + } + + public AppCommandMiddlewareTests() + { + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); + + sut = new AppCommandMiddleware(A.Fake(), assetStore, assetThumbnailGenerator, contextProvider); + } + + [Fact] + public async Task Should_replace_context_app_with_grain_result() + { + var result = A.Fake(); + + var command = CreateCommand(new MyCommand()); + var context = CreateContextForCommand(command); + + context.Complete(result); + + await sut.HandleAsync(context); + + Assert.Same(result, requestContext.App); + } + + [Fact] + public async Task Should_upload_image_to_store() + { + var stream = new MemoryStream(); + + var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); + + var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); + var context = CreateContextForCommand(command); + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(new ImageInfo(100, 100)); + + await sut.HandleAsync(context); + + A.CallTo(() => assetStore.UploadAsync(appId.ToString(), stream, true, A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_throw_exception_when_file_to_upload_is_not_an_image() + { + var stream = new MemoryStream(); + + var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); + + var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); + var context = CreateContextForCommand(command); + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(Task.FromResult(null)); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs new file mode 100644 index 000000000..01adb340b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -0,0 +1,659 @@ +// ========================================================================== +// 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.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppGrainTests : HandlerTestBase + { + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IUser user = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientId = "client"; + private readonly string clientNewName = "My Client"; + private readonly string roleName = "My Role"; + private readonly string planIdPaid = "premium"; + private readonly string planIdFree = "free"; + private readonly AppGrain sut; + private readonly Guid workflowId = Guid.NewGuid(); + private readonly Guid patternId1 = Guid.NewGuid(); + private readonly Guid patternId2 = Guid.NewGuid(); + private readonly Guid patternId3 = Guid.NewGuid(); + private readonly InitialPatterns initialPatterns; + + protected override Guid Id + { + get { return AppId; } + } + + public AppGrainTests() + { + A.CallTo(() => user.Id) + .Returns(contributorId); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId)) + .Returns(user); + + A.CallTo(() => appPlansProvider.GetPlan(A.Ignored)) + .Returns(new ConfigAppLimitsPlan { MaxContributors = 10 }); + + initialPatterns = new InitialPatterns + { + { patternId1, new AppPattern("Number", "[0-9]") }, + { patternId2, new AppPattern("Numbers", "[0-9]*") } + }; + + sut = new AppGrain(initialPatterns, Store, A.Dummy(), appPlansProvider, appPlansBillingManager, userResolver); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Command_should_throw_exception_if_app_is_archived() + { + await ExecuteCreateAsync(); + await ExecuteArchiveAsync(); + + await Assert.ThrowsAsync(ExecuteAttachClientAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateApp { Name = AppName, Actor = Actor, AppId = AppId }; + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(AppName, sut.Snapshot.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }), + CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), + CreateEvent(new AppLanguageAdded { Language = Language.EN }), + CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), + CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) + ); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new UpdateApp { Label = "my-label", Description = "my-description" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal("my-label", sut.Snapshot.Label); + Assert.Equal("my-description", sut.Snapshot.Description); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppUpdated { Label = "my-label", Description = "my-description" }) + ); + } + + [Fact] + public async Task UploadImage_should_create_events_and_update_state() + { + var command = new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => new MemoryStream()) }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal("image/png", sut.Snapshot.Image!.MimeType); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppImageUploaded { Image = sut.Snapshot.Image }) + ); + } + + [Fact] + public async Task RemoveImage_should_create_events_and_update_state() + { + var command = new RemoveAppImage(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Null(sut.Snapshot.Image); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppImageRemoved()) + ); + } + + [Fact] + public async Task ChangePlan_should_create_events_and_update_state() + { + var command = new ChangePlan { PlanId = planIdPaid }; + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .Returns(new PlanChangedResult()); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + Assert.True(result.Value is PlanChangedResult); + + Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) + ); + } + + [Fact] + public async Task ChangePlan_should_reset_plan_for_reset_plan() + { + var command = new ChangePlan { PlanId = planIdFree }; + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .Returns(new PlanChangedResult()); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdFree)) + .Returns(new PlanResetResult()); + + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + Assert.True(result.Value is PlanResetResult); + + Assert.Null(sut.Snapshot.Plan); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanReset()) + ); + } + + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_result() + { + var command = new ChangePlan { PlanId = planIdPaid }; + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + + Assert.Null(sut.Snapshot.Plan); + } + + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task AssignContributor_should_create_events_and_update_state() + { + var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsAdded = true }) + ); + } + + [Fact] + public async Task AssignContributor_should_create_update_events_and_update_state() + { + var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner }; + + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner }) + ); + } + + [Fact] + public async Task RemoveContributor_should_create_events_and_update_state() + { + var command = new RemoveContributor { ContributorId = contributorId }; + + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) + ); + } + + [Fact] + public async Task AttachClient_should_create_events_and_update_state() + { + var command = new AttachClient { Id = clientId }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) + ); + } + + [Fact] + public async Task UpdateClient_should_create_events_and_update_state() + { + var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), + CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer }) + ); + } + + [Fact] + public async Task RevokeClient_should_create_events_and_update_state() + { + var command = new RevokeClient { Id = clientId }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRevoked { Id = clientId }) + ); + } + + [Fact] + public async Task AddWorkflow_should_create_events_and_update_state() + { + var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) + ); + } + + [Fact] + public async Task UpdateWorkflow_should_create_events_and_update_state() + { + var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) + ); + } + + [Fact] + public async Task DeleteWorkflow_should_create_events_and_update_state() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Empty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) + ); + } + + [Fact] + public async Task AddLanguage_should_create_events_and_update_state() + { + var command = new AddLanguage { Language = Language.DE }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageAdded { Language = Language.DE }) + ); + } + + [Fact] + public async Task RemoveLanguage_should_create_events_and_update_state() + { + var command = new RemoveLanguage { Language = Language.DE }; + + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageRemoved { Language = Language.DE }) + ); + } + + [Fact] + public async Task UpdateLanguage_should_create_events_and_update_state() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) + ); + } + + [Fact] + public async Task AddRole_should_create_events_and_update_state() + { + var command = new AddRole { Name = roleName }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(1, sut.Snapshot.Roles.CustomCount); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleAdded { Name = roleName }) + ); + } + + [Fact] + public async Task DeleteRole_should_create_events_and_update_state() + { + var command = new DeleteRole { Name = roleName }; + + await ExecuteCreateAsync(); + await ExecuteAddRoleAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(0, sut.Snapshot.Roles.CustomCount); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleDeleted { Name = roleName }) + ); + } + + [Fact] + public async Task UpdateRole_should_create_events_and_update_state() + { + var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" } }; + + await ExecuteCreateAsync(); + await ExecuteAddRoleAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = new[] { "clients.read" } }) + ); + } + + [Fact] + public async Task AddPattern_should_create_events_and_update_state() + { + var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) + ); + } + + [Fact] + public async Task DeletePattern_should_create_events_and_update_state() + { + var command = new DeletePattern { PatternId = patternId3 }; + + await ExecuteCreateAsync(); + await ExecuteAddPatternAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternDeleted { PatternId = patternId3 }) + ); + } + + [Fact] + public async Task UpdatePattern_should_create_events_and_update_state() + { + var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; + + await ExecuteCreateAsync(); + await ExecuteAddPatternAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) + ); + } + + [Fact] + public async Task ArchiveApp_should_create_events_and_update_state() + { + var command = new ArchiveApp(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppArchived()) + ); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null)) + .MustHaveHappened(); + } + + private Task ExecuteAddPatternAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" })); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName })); + } + + private Task ExecuteAssignContributorAsync() + { + return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Role = Role.Editor })); + } + + private Task ExecuteAttachClientAsync() + { + return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId })); + } + + private Task ExecuteAddRoleAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddRole { Name = roleName })); + } + + private Task ExecuteAddLanguageAsync(Language language) + { + return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); + } + + private Task ExecuteAddWorkflowAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); + } + + private Task ExecuteChangePlanAsync() + { + return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); + } + + private Task ExecuteArchiveAsync() + { + return sut.ExecuteAsync(CreateCommand(new ArchiveApp())); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs new file mode 100644 index 000000000..ffd95eb48 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Billing +{ + public class NoopAppPlanBillingManagerTests + { + private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); + + [Fact] + public void Should_not_have_portal() + { + Assert.False(sut.HasPortal); + } + + [Fact] + public async Task Should_do_nothing_when_changing_plan() + { + await sut.ChangePlanAsync(null!, null!, null); + } + + [Fact] + public async Task Should_not_return_portal_link() + { + Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null!)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs new file mode 100644 index 000000000..3dd7d9ca3 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -0,0 +1,223 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppContributorsTests + { + private readonly IUser user1 = A.Fake(); + private readonly IUser user2 = A.Fake(); + private readonly IUser user3 = A.Fake(); + private readonly IUserResolver users = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly AppContributors contributors_0 = AppContributors.Empty; + private readonly Roles roles = Roles.Empty; + + public GuardAppContributorsTests() + { + A.CallTo(() => user1.Id).Returns("1"); + A.CallTo(() => user2.Id).Returns("2"); + A.CallTo(() => user3.Id).Returns("3"); + + A.CallTo(() => users.FindByIdOrEmailAsync("1")).Returns(user1); + A.CallTo(() => users.FindByIdOrEmailAsync("2")).Returns(user2); + A.CallTo(() => users.FindByIdOrEmailAsync("3")).Returns(user3); + + A.CallTo(() => users.FindByIdOrEmailAsync("1@email.com")).Returns(user1); + A.CallTo(() => users.FindByIdOrEmailAsync("2@email.com")).Returns(user2); + A.CallTo(() => users.FindByIdOrEmailAsync("3@email.com")).Returns(user3); + + A.CallTo(() => users.FindByIdOrEmailAsync("notfound")) + .Returns(Task.FromResult(null)); + + A.CallTo(() => appPlan.MaxContributors) + .Returns(10); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), + new ValidationError("Contributor id is required.", "ContributorId")); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_role_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), + new ValidationError("Role is not a valid value.", "Role")); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_already_exists_with_same_role() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan), + new ValidationError("Contributor has already this role.", "Role")); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IsRestore = true }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + + await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_not_found() + { + var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; + + await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_user_is_actor() + { + var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") }; + + await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); + } + + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_max_reached() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "3" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan), + new ValidationError("You have reached the maximum number of contributors for your plan.")); + } + + [Fact] + public async Task CanAssign_assign_if_if_user_added_by_email() + { + var command = new AssignContributor { ContributorId = "1@email.com" }; + + await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); + + Assert.Equal("1", command.ContributorId); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_found() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(-1); + + var command = new AssignContributor { ContributorId = "1" }; + + await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_has_another_role() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Developer); + + await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("2", Role.Developer); + + await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); + } + + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_from_restore() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "3", IsRestore = true }; + + var contributors_1 = contributors_0.Assign("1", Role.Editor); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); + + ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command), + new ValidationError("Contributor id is required.", "ContributorId")); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); + + ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command), + new ValidationError("Cannot remove the only owner.")); + } + + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); + + GuardAppContributors.CanRemove(contributors_2, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs new file mode 100644 index 000000000..ed688e8f4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs @@ -0,0 +1,165 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppRolesTests + { + private readonly string roleName = "Role1"; + private readonly Roles roles_0 = Roles.Empty; + private readonly AppContributors contributors = AppContributors.Empty; + private readonly AppClients clients = AppClients.Empty; + + [Fact] + public void CanAdd_should_throw_exception_if_name_empty() + { + var command = new AddRole { Name = null! }; + + ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_0, command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_exists() + { + var roles_1 = roles_0.Add(roleName); + + var command = new AddRole { Name = roleName }; + + ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_1, command), + new ValidationError("A role with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddRole { Name = roleName }; + + GuardAppRoles.CanAdd(roles_0, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_name_empty() + { + var command = new DeleteRole { Name = null! }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanDelete_should_throw_exception_if_role_not_found() + { + var command = new DeleteRole { Name = roleName }; + + Assert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients)); + } + + [Fact] + public void CanDelete_should_throw_exception_if_contributor_found() + { + var roles_1 = roles_0.Add(roleName); + + var command = new DeleteRole { Name = roleName }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors.Assign("1", roleName), clients), + new ValidationError("Cannot remove a role when a contributor is assigned.")); + } + + [Fact] + public void CanDelete_should_throw_exception_if_client_found() + { + var roles_1 = roles_0.Add(roleName); + + var command = new DeleteRole { Name = roleName }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), + new ValidationError("Cannot remove a role when a client is assigned.")); + } + + [Fact] + public void CanDelete_should_throw_exception_if_default_role() + { + var roles_1 = roles_0.Add(Role.Developer); + + var command = new DeleteRole { Name = Role.Developer }; + + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients), + new ValidationError("Cannot delete a default role.")); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_command_is_valid() + { + var roles_1 = roles_0.Add(roleName); + + var command = new DeleteRole { Name = roleName }; + + GuardAppRoles.CanDelete(roles_1, command, contributors, clients); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_name_empty() + { + var roles_1 = roles_0.Add(roleName); + + var command = new UpdateRole { Name = null!, Permissions = new[] { "P1" } }; + + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_permission_is_null() + { + var roles_1 = roles_0.Add(roleName); + + var command = new UpdateRole { Name = roleName, Permissions = null! }; + + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), + new ValidationError("Permissions is required.", "Permissions")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_default_role() + { + var roles_1 = roles_0.Add(Role.Developer); + + var command = new UpdateRole { Name = Role.Developer, Permissions = new[] { "P1" } }; + + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), + new ValidationError("Cannot update a default role.")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_role_does_not_exists() + { + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + + Assert.Throws(() => GuardAppRoles.CanUpdate(roles_0, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() + { + var roles_1 = roles_0.Add(roleName); + + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + + GuardAppRoles.CanUpdate(roles_1, command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs new file mode 100644 index 000000000..a1e2d7605 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppTests + { + private readonly IUserResolver users = A.Fake(); + private readonly IAppPlansProvider appPlans = A.Fake(); + private readonly IAppLimitsPlan basicPlan = A.Fake(); + private readonly IAppLimitsPlan freePlan = A.Fake(); + + public GuardAppTests() + { + A.CallTo(() => users.FindByIdOrEmailAsync(A.Ignored)) + .Returns(A.Dummy()); + + A.CallTo(() => appPlans.GetPlan("notfound")) + .Returns(null!); + + A.CallTo(() => appPlans.GetPlan("basic")) + .Returns(basicPlan); + + A.CallTo(() => appPlans.GetPlan("free")) + .Returns(freePlan); + } + + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateApp { Name = "INVALID NAME" }; + + ValidationAssert.Throws(() => GuardApp.CanCreate(command), + new ValidationError("Name is not a valid slug.", "Name")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_app_name_is_valid() + { + var command = new CreateApp { Name = "new-app" }; + + GuardApp.CanCreate(command); + } + + [Fact] + public void CanUploadImage_should_throw_exception_if_name_not_valid() + { + var command = new UploadAppImage(); + + ValidationAssert.Throws(() => GuardApp.CanUploadImage(command), + new ValidationError("File is required.", "File")); + } + + [Fact] + public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() + { + var command = new UploadAppImage { File = new AssetFile("file.png", "image/png", 100, () => new MemoryStream()) }; + + GuardApp.CanUploadImage(command); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_is_null() + { + var command = new ChangePlan { Actor = new RefToken("user", "me") }; + + AppPlan? plan = null; + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("Plan id is required.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + var command = new ChangePlan { PlanId = "notfound", Actor = new RefToken("user", "me") }; + + AppPlan? plan = null; + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("A plan with this id does not exist.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() + { + var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "other"), "premium"); + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("Plan can only changed from the user who configured the plan initially.")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_is_the_same() + { + var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(command.Actor, "basic"); + + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), + new ValidationError("App has already this plan.")); + } + + [Fact] + public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() + { + var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(command.Actor, "premium"); + + GuardApp.CanChangePlan(command, plan, appPlans); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs new file mode 100644 index 000000000..02cac699d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs @@ -0,0 +1,213 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppWorkflowTests + { + private readonly Guid workflowId = Guid.NewGuid(); + private readonly Workflows workflows; + + public GuardAppWorkflowTests() + { + workflows = Workflows.Empty.Add(workflowId, "name"); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_is_not_defined() + { + var command = new AddWorkflow(); + + ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddWorkflow { Name = "my-workflow" }; + + GuardAppWorkflows.CanAdd(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_not_found() + { + var command = new UpdateWorkflow + { + Workflow = Workflow.Empty, + WorkflowId = Guid.NewGuid() + }; + + Assert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() + { + var command = new UpdateWorkflow { WorkflowId = workflowId }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Workflow is required.", "Workflow")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + default, + new Dictionary + { + [Status.Published] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Initial step is required.", "Workflow.Initial")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_initial_step_is_published() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Published, + new Dictionary + { + [Status.Published] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Workflow must have a published step.", "Workflow.Steps")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Published] = null!, + [Status.Draft] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Step is required.", "Workflow.Steps.Published")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition() + }), + [Status.Draft] = new WorkflowStep() + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() + { + var command = new UpdateWorkflow + { + Workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = + new WorkflowStep(), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = null! + }) + }), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), + new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() + { + var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; + + GuardAppWorkflows.CanUpdate(workflows, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_workflow_not_found() + { + var command = new DeleteWorkflow { WorkflowId = Guid.NewGuid() }; + + Assert.Throws(() => GuardAppWorkflows.CanDelete(workflows, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_workflow_is_found() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; + + GuardAppWorkflows.CanDelete(workflows, command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs new file mode 100644 index 000000000..7b6468ead --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -0,0 +1,387 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAppsByNameIndexGrain indexByName = A.Fake(); + private readonly IAppsByUserIndexGrain indexByUser = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly string userId = "user-1"; + private readonly AppsIndex sut; + + public AppsIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(indexByName); + + A.CallTo(() => grainFactory.GetGrain(userId, null)) + .Returns(indexByUser); + + sut = new AppsIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_all_apps_from_user_permissions() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new string[] { appId.Name }))) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_all_apps_from_user() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByUser.GetIdsAsync()) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_all_apps() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdsAsync()) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsAsync(); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_app_by_name() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdAsync(appId.Name)) + .Returns(appId.Id); + + var actual = await sut.GetAppByNameAsync(appId.Name); + + Assert.Same(expected, actual); + } + + [Fact] + public async Task Should_resolve_app_by_id() + { + var expected = SetupApp(0, false); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Same(expected, actual); + } + + [Fact] + public async Task Should_return_null_if_app_archived() + { + SetupApp(0, true); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_return_null_if_app_not_created() + { + SetupApp(-1, false); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_add_app_to_indexes_on_create() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(appId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_app_to_user_index_if_app_created_by_client() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(CreateFromClient(appId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_clear_reservation_when_app_creation_failed() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(CreateFromClient(appId.Name), commandBus); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_indexes_on_create_if_name_taken() + { + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(Task.FromResult(null)); + + var context = + new CommandContext(Create(appId.Name), commandBus) + .Complete(); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => indexByName.AddAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_indexes_on_create_if_name_invalid() + { + var context = + new CommandContext(Create("INVALID"), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_app_to_index_on_contributor_assignment() + { + var context = + new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_from_user_index_on_remove_of_contributor() + { + var context = + new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_app_from_indexes_on_archive() + { + var app = SetupApp(0, false); + + var context = + new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.RemoveAsync(appId.Id)) + .MustHaveHappened(); + + A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding_for_contributors1() + { + var apps = new HashSet(); + + await sut.RebuildByContributorsAsync(userId, apps); + + A.CallTo(() => indexByUser.RebuildAsync(apps)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding_for_contributors2() + { + var users = new HashSet { userId }; + + await sut.RebuildByContributorsAsync(appId.Id, users); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var apps = new Dictionary(); + + await sut.RebuildAsync(apps); + + A.CallTo(() => indexByName.RebuildAsync(apps)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_reserveration() + { + await sut.AddAsync("token"); + + A.CallTo(() => indexByName.AddAsync("token")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_remove_reservation() + { + await sut.RemoveReservationAsync("token"); + + A.CallTo(() => indexByName.RemoveReservationAsync("token")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_request_for_ids() + { + await sut.GetIdsAsync(); + + A.CallTo(() => indexByName.GetIdsAsync()) + .MustHaveHappened(); + } + + private IAppEntity SetupApp(long version, bool archived) + { + var appEntity = A.Fake(); + + A.CallTo(() => appEntity.Name) + .Returns(appId.Name); + A.CallTo(() => appEntity.Version) + .Returns(version); + A.CallTo(() => appEntity.IsArchived) + .Returns(archived); + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); + + var appGrain = A.Fake(); + + A.CallTo(() => appGrain.GetStateAsync()) + .Returns(J.Of(appEntity)); + + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(appGrain); + + return appEntity; + } + + private CreateApp Create(string name) + { + return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorSubject() }; + } + + private CreateApp CreateFromClient(string name) + { + return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorClient() }; + } + + private RefToken ActorSubject() + { + return new RefToken(RefTokenType.Subject, userId); + } + + private RefToken ActorClient() + { + return new RefToken(RefTokenType.Client, userId); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/AlwaysCreateClientCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..ba32701fc --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IAssetLoader assetLoader = A.Fake(); + private readonly IRuleTriggerHandler sut; + + public AssetChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new AssetCreated(), EnrichedAssetEventType.Created }, + new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }, + new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated }, + new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12)) + .Returns(new AssetEntity()); + + var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedAssetEvent; + + Assert.Equal(type, result!.Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new AssetCreated(), trigger, Guid.NewGuid()); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedContentEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForCondition("true", trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForCondition("false", trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.False(result); + }); + } + + private void TestForCondition(string condition, Action action) + { + var trigger = new AssetChangedTriggerV2 { Condition = condition }; + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs new file mode 100644 index 000000000..f07531700 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.IO; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Assets; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class FileTypeTagGeneratorTests + { + private readonly HashSet tags = new HashSet(); + private readonly FileTypeTagGenerator sut = new FileTypeTagGenerator(); + + [Fact] + public void Should_not_add_tag_if_no_file_info() + { + var command = new CreateAsset(); + + sut.GenerateTags(command, tags); + + Assert.Empty(tags); + } + + [Fact] + public void Should_add_file_type() + { + var command = new CreateAsset + { + File = new AssetFile("File.DOCX", "Mime", 100, () => new MemoryStream()) + }; + + sut.GenerateTags(command, tags); + + Assert.Contains("type/docx", tags); + } + + [Fact] + public void Should_add_blob_if_without_extension() + { + var command = new CreateAsset + { + File = new AssetFile("File", "Mime", 100, () => new MemoryStream()) + }; + + sut.GenerateTags(command, tags); + + Assert.Contains("type/blob", tags); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageTagGeneratorTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs new file mode 100644 index 000000000..a5d5c3c77 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs @@ -0,0 +1,236 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using FakeItEasy; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NodaTime.Text; +using Squidex.Domain.Apps.Entities.MongoDb.Assets; +using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Validation; +using Xunit; +using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; +using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; + +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +{ + public class MongoDbQueryTests + { + private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; + private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + + static MongoDbQueryTests() + { + InstantSerializer.Register(); + } + + [Fact] + public void Should_throw_exception_for_full_text_search() + { + Assert.Throws(() => Q(new ClrQuery { FullText = "Full Text" })); + } + + [Fact] + public void Should_make_query_with_lastModified() + { + var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); + var o = C("{ 'mb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_created() + { + var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_createdBy() + { + var i = F(ClrFilter.Eq("createdBy", "Me")); + var o = C("{ 'cb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_version() + { + var i = F(ClrFilter.Eq("version", 0)); + var o = C("{ 'vs' : NumberLong(0) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_fileVersion() + { + var i = F(ClrFilter.Eq("fileVersion", 2)); + var o = C("{ 'fv' : NumberLong(2) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_tags() + { + var i = F(ClrFilter.Eq("tags", "tag1")); + var o = C("{ 'td' : 'tag1' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_fileName() + { + var i = F(ClrFilter.Eq("fileName", "Logo.png")); + var o = C("{ 'fn' : 'Logo.png' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_isImage() + { + var i = F(ClrFilter.Eq("isImage", true)); + var o = C("{ 'im' : true }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_mimeType() + { + var i = F(ClrFilter.Eq("mimeType", "text/json")); + var o = C("{ 'mm' : 'text/json' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_fileSize() + { + var i = F(ClrFilter.Eq("fileSize", 1024)); + var o = C("{ 'fs' : NumberLong(1024) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_pixelHeight() + { + var i = F(ClrFilter.Eq("pixelHeight", 600)); + var o = C("{ 'ph' : 600 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_pixelWidth() + { + var i = F(ClrFilter.Eq("pixelWidth", 800)); + var o = C("{ 'pw' : 800 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = S(SortBuilder.Descending("lastModified")); + var o = C("{ 'mt' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var i = S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy")); + var o = C("{ 'mt' : 1, 'mb' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_take_statement() + { + var query = new ClrQuery { Take = 3 }; + var cursor = A.Fake>(); + + cursor.AssetTake(query.AdjustToModel()); + + A.CallTo(() => cursor.Limit(3)) + .MustHaveHappened(); + } + + [Fact] + public void Should_make_skip_statement() + { + var query = new ClrQuery { Skip = 3 }; + var cursor = A.Fake>(); + + cursor.AssetSkip(query.AdjustToModel()); + + A.CallTo(() => cursor.Skip(3)) + .MustHaveHappened(); + } + + private static string C(string value) + { + return value.Replace('\'', '"'); + } + + private static string F(FilterNode filter) + { + return Q(new ClrQuery { Filter = filter }); + } + + private static string S(params SortNode[] sorts) + { + var cursor = A.Fake>(); + + var i = string.Empty; + + A.CallTo(() => cursor.Sort(A>.Ignored)) + .Invokes((SortDefinition sortDefinition) => + { + i = sortDefinition.Render(Serializer, Registry).ToString(); + }); + + cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); + + return i; + } + + private static string Q(ClrQuery query) + { + var rendered = + query.AdjustToModel().BuildFilter(false).Filter! + .Render(Serializer, Registry).ToString(); + + return rendered; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs new file mode 100644 index 000000000..b04d6c395 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class AssetLoaderTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAssetGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly AssetLoader sut; + + public AssetLoaderTests() + { + A.CallTo(() => grainFactory.GetGrain(id, null)) + .Returns(grain); + + sut = new AssetLoader(grainFactory); + } + + [Fact] + public async Task Should_throw_exception_if_no_state_returned() + { + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(null!)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_throw_exception_if_state_has_other_version() + { + var content = new AssetEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_return_content_from_state() + { + var content = new AssetEntity { Version = 10 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + var result = await sut.GetAsync(id, 10); + + Assert.Same(content, result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs new file mode 100644 index 000000000..e0900ded3 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure.Queries; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class FilterTagTransformerTests + { + private readonly ITagService tagService = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + + [Fact] + public void Should_normalize_tags() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) + .Returns(new Dictionary { ["name1"] = "id1" }); + + var source = ClrFilter.Eq("tags", "name1"); + + var result = FilterTagTransformer.Transform(source, appId, tagService); + + Assert.Equal("tags == 'id1'", result!.ToString()); + } + + [Fact] + public void Should_not_fail_when_tags_not_found() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) + .Returns(new Dictionary()); + + var source = ClrFilter.Eq("tags", "name1"); + + var result = FilterTagTransformer.Transform(source, appId, tagService); + + Assert.Equal("tags == 'name1'", result!.ToString()); + } + + [Fact] + public void Should_not_normalize_other_field() + { + var source = ClrFilter.Eq("other", "value"); + + var result = FilterTagTransformer.Transform(source, appId, tagService); + + Assert.Equal("other == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..39d41bbe0 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -0,0 +1,235 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private +#pragma warning disable RECS0070 + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IContentLoader contentLoader = A.Fake(); + private readonly IRuleTriggerHandler sut; + private readonly Guid ruleId = Guid.NewGuid(); + private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); + private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); + + public ContentChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new ContentCreated(), EnrichedContentEventType.Created }, + new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, + new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, + new object[] { new ContentStatusChanged { Change = StatusChange.Change }, EnrichedContentEventType.StatusChanged }, + new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, + new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) + .Returns(new ContentEntity { SchemaId = SchemaMatch }); + + var result = await sut.CreateEnrichedEventAsync(envelope) as EnrichedContentEvent; + + Assert.Equal(type, result!.Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new AssetCreated(), trigger, ruleId); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_precheck_when_trigger_contains_no_schemas() + { + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_handling_all_events() + { + TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_condition_is_empty() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_precheck_when_schema_id_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_trigger_contains_no_schemas() + { + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_handling_all_events() + { + TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_schema_id_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => + { + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + + Assert.False(result); + }); + } + + private void TestForTrigger(bool handleAll, NamedId? schemaId, string? condition, Action action) + { + var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; + + if (schemaId != null) + { + trigger.Schemas = new ReadOnlyCollection(new List + { + new ContentChangedTriggerSchemaV2 + { + SchemaId = schemaId.Id, Condition = condition + } + }); + } + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs new file mode 100644 index 000000000..77f68ed0b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -0,0 +1,592 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentGrainTests : HandlerTestBase + { + private readonly Guid contentId = Guid.NewGuid(); + private readonly IActivationLimit limit = A.Fake(); + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake(); + private readonly IContentRepository contentRepository = A.Dummy(); + private readonly IContentWorkflow contentWorkflow = A.Fake(x => x.Wrapping(new DefaultContentWorkflow())); + private readonly ISchemaEntity schema; + private readonly IScriptEngine scriptEngine = A.Fake(); + + private readonly NamedContentData invalidData = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", null)) + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData data = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData patch = + new NamedContentData() + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly NamedContentData otherData = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly NamedContentData patched; + private readonly ContentGrain sut; + + protected override Guid Id + { + get { return contentId; } + } + + public ContentGrainTests() + { + app = Mocks.App(AppNamedId, Language.DE); + + var scripts = new SchemaScripts + { + Change = "", + Create = "", + Delete = "", + Update = "" + }; + + var schemaDef = + new Schema("my-schema") + .AddNumber(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true }) + .AddNumber(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false }) + .ConfigureScripts(scripts); + + schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); + + A.CallTo(() => appProvider.GetAppAsync(AppName)) + .Returns(app); + + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) + .Returns((app, schema)); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .ReturnsLazily(x => x.GetArgument(0).Data!); + + patched = patch.MergeInto(data); + + sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository, limit); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public void Should_set_limit() + { + A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) + .MustHaveHappened(); + } + + [Fact] + public async Task Command_should_throw_exception_if_content_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecuteUpdateAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateContent { Data = data }; + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_also_publish() + { + var command = new CreateContent { Data = data, Publish = true }; + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Published, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Create_should_throw_when_invalid_data_is_passed() + { + var command = new CreateContent { Data = invalidData }; + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new UpdateContent { Data = otherData }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_create_proposal_events_and_update_state() + { + var command = new UpdateContent { Data = otherData, AsDraft = true }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdateProposed { Data = otherData }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_not_create_event_for_same_data() + { + var command = new UpdateContent { Data = data }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Single(LastEvents); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_when_invalid_data_is_passed() + { + var command = new UpdateContent { Data = invalidData }; + + await ExecuteCreateAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + } + + [Fact] + public async Task Patch_should_create_events_and_update_state() + { + var command = new PatchContent { Data = patch }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_create_proposal_events_and_update_state() + { + var command = new PatchContent { Data = patch, AsDraft = true }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.True(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdateProposed { Data = patched }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_not_create_event_for_same_data() + { + var command = new PatchContent { Data = data }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Single(LastEvents); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Published, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state_when_archived() + { + var command = new ChangeContentStatus { Status = Status.Archived }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Archived, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state_when_unpublished() + { + var command = new ChangeContentStatus { Status = Status.Draft }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Unpublished, Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Published), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_events_and_update_state_when_restored() + { + var command = new ChangeContentStatus { Status = Status.Draft }; + + await ExecuteCreateAsync(); + await ExecuteArchiveAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Archived), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_create_proposal_events_and_update_state() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteProposeUpdateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.False(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentChangesPublished()) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() + { + var dueTime = Instant.MaxValue; + + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob!.Status); + Assert.Equal(dueTime, sut.Snapshot.ScheduleJob.DueTime); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_refresh_properties_and_revert_scheduling_when_invoked_by_scheduler() + { + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Published, Instant.MaxValue); + + var command = new ChangeContentStatus { Status = Status.Published, JobId = sut.Snapshot.ScheduleJob!.Id }; + + A.CallTo(() => contentWorkflow.CanMoveToAsync(A.Ignored, Status.Published, User)) + .Returns(false); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Null(sut.Snapshot.ScheduleJob); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentSchedulingCancelled()) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Delete_should_update_properties_and_create_events() + { + var command = new DeleteContent(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + + A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft), "")) + .MustHaveHappened(); + } + + [Fact] + public async Task DiscardChanges_should_update_properties_and_create_events() + { + var command = new DiscardChanges(); + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteProposeUpdateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(3)); + + Assert.False(sut.Snapshot.IsPending); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentChangesDiscarded()) + ); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data })); + } + + private Task ExecuteUpdateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData })); + } + + private Task ExecuteProposeUpdateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true })); + } + + private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) + { + return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime })); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new DeleteContent())); + } + + private Task ExecuteArchiveAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Archived })); + } + + private Task ExecutePublishAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); + } + + private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus) + { + return A.That.Matches(x => M(x, newData, oldData, newStatus, default)); + } + + private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus) + { + return A.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus)); + } + + private bool M(ScriptContext x, NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus) + { + return + Equals(x.Data, newData) && + Equals(x.DataOld, oldData) && + Equals(x.Status, newStatus) && + Equals(x.StatusOld, oldStatus) && + x.ContentId == contentId && x.User == User; + } + + protected T CreateContentEvent(T @event) where T : ContentEvent + { + @event.ContentId = contentId; + + return CreateEvent(@event); + } + + protected T CreateContentCommand(T command) where T : ContentCommand + { + command.ContentId = contentId; + + return CreateCommand(command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs new file mode 100644 index 000000000..093d7da92 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DefaultContentWorkflowTests + { + private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); + + [Fact] + public async Task Should_always_allow_publish_on_create() + { + var result = await sut.CanPublishOnCreateAsync(null!, null!, null!); + + Assert.True(result); + } + + [Fact] + public async Task Should_draft_as_initial_status() + { + var expected = new StatusInfo(Status.Draft, StatusColors.Draft); + + var result = await sut.GetInitialStatusAsync(null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_check_is_valid_next() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanMoveToAsync(content, Status.Draft, null!); + + Assert.True(result); + } + + [Fact] + public async Task Should_be_able_to_update_published() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_be_able_to_update_draft() + { + var content = new ContentEntity { Status = Status.Published }; + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_be_able_to_update_archived() + { + var content = new ContentEntity { Status = Status.Archived }; + + var result = await sut.CanUpdateAsync(content); + + Assert.False(result); + } + + [Fact] + public async Task Should_get_next_statuses_for_draft() + { + var content = new ContentEntity { Status = Status.Draft }; + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = new ContentEntity { Status = Status.Archived }; + + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = new ContentEntity { Status = Status.Published }; + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses() + { + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(null!); + + result.Should().BeEquivalentTo(expected); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs new file mode 100644 index 000000000..73991874e --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DefaultWorkflowsValidatorTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly DefaultWorkflowsValidator sut; + + public DefaultWorkflowsValidatorTests() + { + var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .Returns(Task.FromResult(null)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + + sut = new DefaultWorkflowsValidator(appProvider); + } + + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() + { + var workflows = Workflows.Empty + .Add(Guid.NewGuid(), "workflow1") + .Add(Guid.NewGuid(), "workflow2"); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Equal(errors, new[] { "Multiple workflows cover all schemas." }); + } + + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })) + .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Equal(errors, new[] { "The schema `my-schema` is covered by multiple workflows." }); + } + + [Fact] + public async Task Should_not_generate_error_if_schema_deleted() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var oldSchemaId = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })) + .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_generate_errors_for_no_overlaps() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_generate_errors_for_empty_workflows() + { + var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); + + Assert.Empty(errors); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs new file mode 100644 index 000000000..25b4389fc --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -0,0 +1,352 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DynamicContentWorkflowTests + { + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly NamedId simpleSchemaId = NamedId.Of(Guid.NewGuid(), "my-simple-schema"); + private readonly DynamicContentWorkflow sut; + + private readonly Workflow workflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Archived] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Archived, true), + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Published] = new WorkflowTransition("data.field.iv === 2", ReadOnlyCollection.Create("Owner", "Editor")) + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Published) + }); + + public DynamicContentWorkflowTests() + { + app = Mocks.App(appId); + + var simpleWorkflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Published] = new WorkflowTransition() + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Published) + }, + new List { simpleSchemaId.Id }); + + var workflows = Workflows.Empty.Set(workflow).Set(Guid.NewGuid(), simpleWorkflow); + + A.CallTo(() => appProvider.GetAppAsync(appId.Id)) + .Returns(app); + + A.CallTo(() => app.Workflows) + .Returns(workflows); + + sut = new DynamicContentWorkflow(new JintScriptEngine(), appProvider); + } + + [Fact] + public async Task Should_draft_as_initial_status() + { + var expected = new StatusInfo(Status.Draft, StatusColors.Draft); + + var result = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_allow_publish_on_create() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_publish_on_create_if_data_is_invalid() + { + var content = CreateContent(Status.Draft, 4); + + var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); + + Assert.False(result); + } + + [Fact] + public async Task Should_not_allow_publish_on_create_if_role_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Developer")); + + Assert.False(result); + } + + [Fact] + public async Task Should_check_is_valid_next() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_transition_if_role_is_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Developer")); + + Assert.False(result); + } + + [Fact] + public async Task Should_allow_transition_if_role_is_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_transition_if_data_not_valid() + { + var content = CreateContent(Status.Draft, 4); + + var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); + + Assert.False(result); + } + + [Fact] + public async Task Should_be_able_to_update_published() + { + var content = CreateContent(Status.Published, 2); + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_be_able_to_update_draft() + { + var content = CreateContent(Status.Published, 2); + + var result = await sut.CanUpdateAsync(content); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_be_able_to_update_archived() + { + var content = CreateContent(Status.Archived, 2); + + var result = await sut.CanUpdateAsync(content); + + Assert.False(result); + } + + [Fact] + public async Task Should_get_next_statuses_for_draft() + { + var content = CreateContent(Status.Draft, 2); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived) + }; + + var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Developer")); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_limit_next_statuses_if_expression_does_not_evauate_to_true() + { + var content = CreateContent(Status.Draft, 4); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived) + }; + + var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_limit_next_statuses_if_role_is_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = CreateContent(Status.Archived, 2); + + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = CreateContent(Status.Published, 2); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft) + }; + + var result = await sut.GetNextsAsync(content, null!); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses() + { + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses_for_simple_schema_workflow() + { + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses_for_default_workflow_when_no_workflow_configured() + { + A.CallTo(() => app.Workflows).Returns(Workflows.Empty); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + + result.Should().BeEquivalentTo(expected); + } + + private IContentEntity CreateContent(Status status, int value, bool simple = false) + { + var content = new ContentEntity { AppId = appId, Status = status }; + + if (simple) + { + content.SchemaId = simpleSchemaId; + } + else + { + content.SchemaId = schemaId; + } + + content.DataDraft = + new NamedContentData() + .AddField("field", + new ContentFieldData() + .AddValue("iv", value)); + + return content; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs new file mode 100644 index 000000000..fb9005e86 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -0,0 +1,1270 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLQueriesTests : GraphQLTestBase + { + [Fact] + public async Task Should_introspect() + { + const string query = @" + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + args { + ...InputValue + } + onOperation + onFragment + onField + } + } + } + + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + }"; + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); + + var json = serializer.Serialize(result.Response, true); + + Assert.NotEmpty(json); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_empty_object_for_empty_query(string query) + { + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_assets_when_querying_assets() + { + const string query = @" + query { + queryAssets(filter: ""my-query"", top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileHash + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + tags + slug + } + }"; + + var asset = CreateAsset(Guid.NewGuid()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) + .Returns(ResultList.CreateFrom(0, asset)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssets = new dynamic[] + { + new + { + id = asset.Id, + version = 1, + created = asset.Created, + createdBy = "subject:user1", + lastModified = asset.LastModified, + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileHash = "ABC123", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600, + tags = new[] { "tag1", "tag2" }, + slug = "myfile.png" + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_assets_with_total_when_querying_assets_with_total() + { + const string query = @" + query { + queryAssetsWithTotal(filter: ""my-query"", top: 30, skip: 5) { + total + items { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileHash + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + tags + slug + } + } + }"; + + var asset = CreateAsset(Guid.NewGuid()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) + .Returns(ResultList.CreateFrom(10, asset)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssetsWithTotal = new + { + total = 10, + items = new dynamic[] + { + new + { + id = asset.Id, + version = 1, + created = asset.Created, + createdBy = "subject:user1", + lastModified = asset.LastModified, + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileHash = "ABC123", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600, + tags = new[] { "tag1", "tag2" }, + slug = "myfile.png" + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_null_single_asset() + { + var assetId = Guid.NewGuid(); + + var query = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + .Returns(ResultList.CreateFrom(1)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = (object?)null + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_single_asset_when_finding_asset() + { + var assetId = Guid.NewGuid(); + var asset = CreateAsset(assetId); + + var query = @" + query { + findAsset(id: """") { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileHash + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + tags + slug + } + }".Replace("", assetId.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + .Returns(ResultList.CreateFrom(1, asset)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = new + { + id = asset.Id, + version = 1, + created = asset.Created, + createdBy = "subject:user1", + lastModified = asset.LastModified, + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileHash = "ABC123", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600, + tags = new[] { "tag1", "tag2" }, + slug = "myfile.png" + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_contents_when_querying_contents() + { + const string query = @" + query { + queryMySchemaContents(top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + myLocalized { + de_DE + } + myArray { + iv { + nestedNumber + nestedBoolean + } + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + .Returns(ResultList.CreateFrom(0, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContents = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + }, + myLocalized = new + { + de_DE = "de-DE" + }, + myArray = new + { + iv = new[] + { + new + { + nestedNumber = 10, + nestedBoolean = true + }, + new + { + nestedNumber = 20, + nestedBoolean = false + } + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_multiple_contents_with_total_when_querying_contents_with_total() + { + const string query = @" + query { + queryMySchemaContentsWithTotal(top: 30, skip: 5) { + total + items { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + myLocalized { + de_DE + } + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + .Returns(ResultList.CreateFrom(10, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContentsWithTotal = new + { + total = 10, + items = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + }, + myLocalized = new + { + de_DE = "de-DE" + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_single_content_with_duplicate_names() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + data { + myNumber { + iv + } + myNumber2 { + iv + } + myArray { + iv { + nestedNumber + nestedNumber2 + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + data = new + { + myNumber = new + { + iv = 1 + }, + myNumber2 = new + { + iv = 2 + }, + myArray = new + { + iv = new[] + { + new + { + nestedNumber = 10, + nestedNumber2 = 11 + }, + new + { + nestedNumber = 20, + nestedNumber2 = 21 + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_null_single_content() + { + var contentId = Guid.NewGuid(); + + var query = @" + query { + findMySchemaContent(id: """") { + id + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = (object?)null + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_single_content_when_finding_content() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + myLocalized { + de_DE + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + }, + myLocalized = new + { + de_DE = "de-DE" + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myReferences { + iv { + id + data { + ref1Field { + iv + } + } + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myReferences = new + { + iv = new[] + { + new + { + id = contentRefId, + data = new + { + ref1Field = new + { + iv = "ref1" + } + } + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myUnion { + iv { + ... on Content { + id + } + ... on MyRefSchema1 { + data { + ref1Field { + iv + } + } + } + __typename + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myUnion = new + { + iv = new[] + { + new + { + id = contentRefId, + data = new + { + ref1Field = new + { + iv = "ref1" + } + }, + __typename = "MyRefSchema1" + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() + { + var assetRefId = Guid.NewGuid(); + var assetRef = CreateAsset(assetRefId); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, assetRefId); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myAssets { + iv { + id + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) + .Returns(ResultList.CreateFrom(0, assetRef)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myAssets = new + { + iv = new[] + { + new + { + id = assetRefId + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_make_multiple_queries() + { + var assetId1 = Guid.NewGuid(); + var assetId2 = Guid.NewGuid(); + var asset1 = CreateAsset(assetId1); + var asset2 = CreateAsset(assetId2); + + var query1 = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId1.ToString()); + var query2 = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId2.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1))) + .Returns(ResultList.CreateFrom(0, asset1)); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2))) + .Returns(ResultList.CreateFrom(0, asset2)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); + + var expected = new object[] + { + new + { + data = new + { + findAsset = new + { + id = asset1.Id + } + } + }, + new + { + data = new + { + findAsset = new + { + id = asset2.Id + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_not_return_data_when_field_not_part_of_content() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); + + var query = @" + query { + findMySchemaContent(id: """") { + id + version + created + createdBy + lastModified + lastModifiedBy + url + data { + myInvalid { + iv + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var json = serializer.Serialize(result); + + Assert.Contains("\"data\":null", json); + } + + [Fact] + public async Task Should_return_draft_content_when_querying_dataDraft() + { + var dataDraft = new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "draft value")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 42)); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null, dataDraft); + + var query = @" + query { + findMySchemaContent(id: """") { + dataDraft { + myString { + de + } + myNumber { + iv + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + dataDraft = new + { + myString = new + { + de = "draft value" + }, + myNumber = new + { + iv = 42 + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_return_null_when_querying_dataDraft_and_no_draft_content_is_available() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null); + + var query = @" + query { + findMySchemaContent(id: """") { + dataDraft { + myString { + de + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + dataDraft = (object?)null + } + } + }; + + AssertResult(expected, result); + } + + private static IReadOnlyList MatchId(Guid contentId) + { + return A>.That.Matches(x => x.Count == 1 && x[0] == contentId); + } + + private static Q MatchIdQuery(Guid contentId) + { + return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); + } + + private Context MatchsAssetContext() + { + return A.That.Matches(x => x.App == app && x.User == requestContext.User); + } + + private Context MatchsContentContext() + { + return A.That.Matches(x => x.App == app && x.User == requestContext.User); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs new file mode 100644 index 000000000..c188160f9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -0,0 +1,287 @@ +// ========================================================================== +// 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 FakeItEasy; +using GraphQL; +using GraphQL.DataLoader; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.TestData; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; +using Xunit; + +#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLTestBase + { + protected readonly IAppEntity app; + protected readonly IAssetQueryService assetQuery = A.Fake(); + protected readonly IContentQueryService contentQuery = A.Fake(); + protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); + protected readonly ISchemaEntity schema; + protected readonly ISchemaEntity schemaRef1; + protected readonly ISchemaEntity schemaRef2; + protected readonly Context requestContext; + protected readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + protected readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + protected readonly NamedId schemaRefId1 = NamedId.Of(Guid.NewGuid(), "my-ref-schema1"); + protected readonly NamedId schemaRefId2 = NamedId.Of(Guid.NewGuid(), "my-ref-schema2"); + protected readonly IGraphQLService sut; + + public GraphQLTestBase() + { + app = Mocks.App(appId, Language.DE, Language.GermanGermany); + + var schemaDef = + new Schema(schemaId.Name) + .Publish() + .AddJson(1, "my-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddString(2, "my-string", Partitioning.Language, + new StringFieldProperties()) + .AddNumber(3, "my-number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddNumber(4, "my_number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddAssets(5, "my-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(6, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(7, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties()) + .AddReferences(8, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaRefId1.Id }) + .AddReferences(81, "my-union", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddReferences(9, "my-invalid", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }) + .AddGeolocation(10, "my-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddTags(11, "my-tags", Partitioning.Invariant, + new TagsFieldProperties()) + .AddString(12, "my-localized", Partitioning.Language, + new StringFieldProperties()) + .AddArray(13, "my-array", Partitioning.Invariant, f => f + .AddBoolean(121, "nested-boolean") + .AddNumber(122, "nested-number") + .AddNumber(123, "nested_number")) + .ConfigureScripts(new SchemaScripts { Query = "" }); + + schema = Mocks.Schema(appId, schemaId, schemaDef); + + var schemaRef1Def = + new Schema(schemaRefId1.Name) + .Publish() + .AddString(1, "ref1-field", Partitioning.Invariant); + + schemaRef1 = Mocks.Schema(appId, schemaRefId1, schemaRef1Def); + + var schemaRef2Def = + new Schema(schemaRefId2.Name) + .Publish() + .AddString(1, "ref2-field", Partitioning.Invariant); + + schemaRef2 = Mocks.Schema(appId, schemaRefId2, schemaRef2Def); + + requestContext = new Context(Mocks.FrontendUser(), app); + + sut = CreateSut(); + } + + protected IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData? data = null, NamedContentData? dataDraft = null) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + data ??= + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "value")) + .AddField("my-assets", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(assetId.ToString()))) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 1.0)) + .AddField("my_number", + new ContentFieldData() + .AddValue("iv", 2.0)) + .AddField("my-boolean", + new ContentFieldData() + .AddValue("iv", true)) + .AddField("my-datetime", + new ContentFieldData() + .AddValue("iv", now)) + .AddField("my-tags", + new ContentFieldData() + .AddValue("iv", JsonValue.Array("tag1", "tag2"))) + .AddField("my-references", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(refId.ToString()))) + .AddField("my-union", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(refId.ToString()))) + .AddField("my-geolocation", + new ContentFieldData() + .AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20))) + .AddField("my-json", + new ContentFieldData() + .AddValue("iv", JsonValue.Object().Add("value", 1))) + .AddField("my-localized", + new ContentFieldData() + .AddValue("de-DE", "de-DE")) + .AddField("my-array", + new ContentFieldData() + .AddValue("iv", JsonValue.Array( + JsonValue.Object() + .Add("nested-boolean", true) + .Add("nested-number", 10) + .Add("nested_number", 11), + JsonValue.Object() + .Add("nested-boolean", false) + .Add("nested-number", 20) + .Add("nested_number", 21)))); + + var content = new ContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + Data = data, + DataDraft = dataDraft!, + SchemaId = schemaId, + Status = Status.Draft, + StatusColor = "red" + }; + + return content; + } + + protected static IEnrichedContentEntity CreateRefContent(NamedId schemaId, Guid id, string field, string value) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var data = + new NamedContentData() + .AddField(field, + new ContentFieldData() + .AddValue("iv", value)); + + var content = new ContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + Data = data, + DataDraft = data, + SchemaId = schemaId, + Status = Status.Draft, + StatusColor = "red" + }; + + return content; + } + + protected static IEnrichedAssetEntity CreateAsset(Guid id) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var asset = new AssetEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + FileName = "MyFile.png", + Slug = "myfile.png", + FileSize = 1024, + FileHash = "ABC123", + FileVersion = 123, + MimeType = "image/png", + IsImage = true, + PixelWidth = 800, + PixelHeight = 600, + TagNames = new[] { "tag1", "tag2" }.ToHashSet() + }; + + return asset; + } + + protected void AssertResult(object expected, (bool HasErrors, object Response) result, bool checkErrors = true) + { + if (checkErrors && result.HasErrors) + { + throw new InvalidOperationException(Serialize(result)); + } + + var resultJson = serializer.Serialize(result.Response, true); + var expectJson = serializer.Serialize(expected, true); + + Assert.Equal(expectJson, resultJson); + } + + private string Serialize((bool HasErrors, object Response) result) + { + return serializer.Serialize(result); + } + + private CachingGraphQLService CreateSut() + { + var appProvider = A.Fake(); + + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + .Returns(new List { schema, schemaRef1, schemaRef2 }); + + var dataLoaderContext = new DataLoaderContextAccessor(); + + var services = new Dictionary + { + [typeof(IAppProvider)] = appProvider, + [typeof(IAssetQueryService)] = assetQuery, + [typeof(IContentQueryService)] = contentQuery, + [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, + [typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(), + [typeof(IOptions)] = Options.Create(new AssetOptions()), + [typeof(IOptions)] = Options.Create(new ContentOptions()), + [typeof(ISemanticLog)] = A.Fake(), + [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) + }; + + var resolver = new FuncDependencyResolver(t => services[t]); + + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + return new CachingGraphQLService(cache, resolver); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs new file mode 100644 index 000000000..7d603ae59 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs @@ -0,0 +1,289 @@ +// ========================================================================== +// 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 FakeItEasy; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using NodaTime.Text; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; +using Xunit; +using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; +using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; + +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +{ + public class MongoDbQueryTests + { + private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; + private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly Schema schemaDef; + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + static MongoDbQueryTests() + { + InstantSerializer.Register(); + } + + public MongoDbQueryTests() + { + schemaDef = + new Schema("user") + .AddString(1, "firstName", Partitioning.Language, + new StringFieldProperties()) + .AddString(2, "lastName", Partitioning.Language, + new StringFieldProperties()) + .AddBoolean(3, "isAdmin", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddNumber(4, "age", Partitioning.Invariant, + new NumberFieldProperties()) + .AddDateTime(5, "birthday", Partitioning.Invariant, + new DateTimeFieldProperties()) + .AddAssets(6, "pictures", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddReferences(7, "friends", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(8, "dashed-field", Partitioning.Invariant, + new StringFieldProperties()) + .AddArray(9, "hobbies", Partitioning.Invariant, a => a + .AddString(91, "name")) + .Update(new SchemaProperties()); + + var schema = A.Dummy(); + A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); + A.CallTo(() => schema.Version).Returns(3); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + + var app = A.Dummy(); + A.CallTo(() => app.Id).Returns(Guid.NewGuid()); + A.CallTo(() => app.Version).Returns(3); + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + } + + [Fact] + public void Should_throw_exception_for_invalid_field() + { + Assert.Throws(() => F(ClrFilter.Eq("data/invalid/iv", "Me"))); + } + + [Fact] + public void Should_make_query_with_lastModified() + { + var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); + var o = C("{ 'mb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_created() + { + var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_createdBy() + { + var i = F(ClrFilter.Eq("createdBy", "Me")); + var o = C("{ 'cb' : 'Me' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_version() + { + var i = F(ClrFilter.Eq("version", 0L)); + var o = C("{ 'vs' : NumberLong(0) }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_version_and_list() + { + var i = F(ClrFilter.In("version", new List { 0L, 2L, 5L })); + var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_from_draft() + { + var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"), true); + var o = C("{ 'dd.8.iv' : 'Value' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_empty_test() + { + var i = F(ClrFilter.Empty("data/firstName/iv"), true); + var o = C("{ '$or' : [{ 'dd.1.iv' : { '$exists' : false } }, { 'dd.1.iv' : null }, { 'dd.1.iv' : '' }, { 'dd.1.iv' : [] }] }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_datetime_data() + { + var i = F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); + var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_underscore_field() + { + var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value")); + var o = C("{ 'do.8.iv' : 'Value' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_references_equals() + { + var i = F(ClrFilter.Eq("data/friends/iv", "guid")); + var o = C("{ 'do.7.iv' : 'guid' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_array_field() + { + var i = F(ClrFilter.Eq("data/hobbies/iv/name", "PC")); + var o = C("{ 'do.9.iv.91' : 'PC' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_assets_equals() + { + var i = F(ClrFilter.Eq("data/pictures/iv", "guid")); + var o = C("{ 'do.6.iv' : 'guid' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_query_with_full_text() + { + var i = Q(new ClrQuery { FullText = "Hello my World" }); + var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = S(SortBuilder.Descending("data/age/iv")); + var o = C("{ 'do.4.iv' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); + var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_take_statement() + { + var query = new ClrQuery { Take = 3 }; + var cursor = A.Fake>(); + + cursor.ContentTake(query.AdjustToModel(schemaDef, false)); + + A.CallTo(() => cursor.Limit(3)) + .MustHaveHappened(); + } + + [Fact] + public void Should_make_skip_statement() + { + var query = new ClrQuery { Skip = 3 }; + var cursor = A.Fake>(); + + cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); + + A.CallTo(() => cursor.Skip(3)) + .MustHaveHappened(); + } + + private static string C(string value) + { + return value.Replace('\'', '"'); + } + + private string F(FilterNode filter, bool useDraft = false) + { + return Q(new ClrQuery { Filter = filter }, useDraft); + } + + private string S(params SortNode[] sorts) + { + var cursor = A.Fake>(); + + var i = string.Empty; + + A.CallTo(() => cursor.Sort(A>.Ignored)) + .Invokes((SortDefinition sortDefinition) => + { + i = sortDefinition.Render(Serializer, Registry).ToString(); + }); + + cursor.ContentSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); + + return i; + } + + private string Q(ClrQuery query, bool useDraft = false) + { + var rendered = + query.AdjustToModel(schemaDef, useDraft).BuildFilter().Filter! + .Render(Serializer, Registry).ToString(); + + return rendered; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs new file mode 100644 index 000000000..36962b4cd --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -0,0 +1,204 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentEnricherTests + { + private readonly IContentWorkflow contentWorkflow = A.Fake(); + private readonly IContentQueryService contentQuery = A.Fake(); + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ContentEnricher sut; + + public ContentEnricherTests() + { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A.Ignored, schemaId.Id.ToString())) + .Returns(schema); + + sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); + } + + [Fact] + public async Task Should_add_app_version_and_schema_as_dependency() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Contains(requestContext.App.Version, result.CacheDependencies); + + Assert.Contains(schema.Id, result.CacheDependencies); + Assert.Contains(schema.Version, result.CacheDependencies); + } + + [Fact] + public async Task Should_enrich_with_reference_fields() + { + var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, ctx); + + Assert.NotNull(result.ReferenceFields); + } + + [Fact] + public async Task Should_not_enrich_with_reference_fields_when_not_frontend() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Null(result.ReferenceFields); + } + + [Fact] + public async Task Should_enrich_with_schema_names() + { + var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, ctx); + + Assert.Equal("my-schema", result.SchemaName); + Assert.Equal("my-schema", result.SchemaDisplayName); + } + + [Fact] + public async Task Should_not_enrich_with_schema_names_when_not_frontend() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Null(result.SchemaName); + Assert.Null(result.SchemaDisplayName); + } + + [Fact] + public async Task Should_enrich_content_with_status_color() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal(StatusColors.Published, result.StatusColor); + } + + [Fact] + public async Task Should_enrich_content_with_default_color_if_not_found() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(Task.FromResult(null!)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal(StatusColors.Draft, result.StatusColor); + } + + [Fact] + public async Task Should_enrich_content_with_can_update() + { + requestContext.WithResolveFlow(true); + + var source = new ContentEntity { SchemaId = schemaId }; + + A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) + .Returns(true); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.True(result.CanUpdate); + } + + [Fact] + public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() + { + requestContext.WithResolveFlow(false); + + var source = new ContentEntity { SchemaId = schemaId }; + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.False(result.CanUpdate); + + A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_enrich_multiple_contents_and_cache_color() + { + var source1 = PublishedContent(); + var source2 = PublishedContent(); + + var source = new IContentEntity[] + { + source1, + source2 + }; + + A.CallTo(() => contentWorkflow.GetInfoAsync(source1)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Equal(StatusColors.Published, result[0].StatusColor); + Assert.Equal(StatusColors.Published, result[1].StatusColor); + + A.CallTo(() => contentWorkflow.GetInfoAsync(A.Ignored)) + .MustHaveHappenedOnceExactly(); + } + + private ContentEntity PublishedContent() + { + return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs new file mode 100644 index 000000000..09a1dd703 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentLoaderTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IContentGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly ContentLoader sut; + + public ContentLoaderTests() + { + A.CallTo(() => grainFactory.GetGrain(id, null)) + .Returns(grain); + + sut = new ContentLoader(grainFactory); + } + + [Fact] + public async Task Should_throw_exception_if_no_state_returned() + { + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(null!)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_throw_exception_if_state_has_other_version() + { + var content = new ContentEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_not_throw_exception_if_state_has_other_version_than_any() + { + var content = new ContentEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(EtagVersion.Any)) + .Returns(J.Of(content)); + + await sut.GetAsync(id, EtagVersion.Any); + } + + [Fact] + public async Task Should_return_content_from_state() + { + var content = new ContentEntity { Version = 10 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + var result = await sut.GetAsync(id, 10); + + Assert.Same(content, result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs new file mode 100644 index 000000000..80a5e9da7 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -0,0 +1,503 @@ +// ========================================================================== +// 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 System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class ContentQueryServiceTests + { + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAssetUrlGenerator urlGenerator = A.Fake(); + private readonly IContentEnricher contentEnricher = A.Fake(); + private readonly IContentRepository contentRepository = A.Fake(); + private readonly IContentLoader contentVersionLoader = A.Fake(); + private readonly ISchemaEntity schema; + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly Guid contentId = Guid.NewGuid(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly NamedContentData contentData = new NamedContentData(); + private readonly NamedContentData contentTransformed = new NamedContentData(); + private readonly ClaimsPrincipal user; + private readonly ClaimsIdentity identity = new ClaimsIdentity(); + private readonly Context requestContext; + private readonly ContentQueryParser queryParser = A.Fake(); + private readonly ContentQueryService sut; + + public static IEnumerable ApiStatusTests = new[] + { + new object?[] { 0, new[] { Status.Published } }, + new object?[] { 1, null } + }; + + public ContentQueryServiceTests() + { + user = new ClaimsPrincipal(identity); + + app = Mocks.App(appId); + + requestContext = new Context(user, app); + + var schemaDef = + new Schema(schemaId.Name) + .ConfigureScripts(new SchemaScripts { Query = "" }); + + schema = Mocks.Schema(appId, schemaId, schemaDef); + + SetupEnricher(); + + A.CallTo(() => queryParser.ParseQuery(requestContext, schema, A.Ignored)) + .Returns(new ClrQuery()); + + sut = new ContentQueryService( + appProvider, + urlGenerator, + contentEnricher, + contentRepository, + contentVersionLoader, + scriptEngine, + queryParser); + } + + [Fact] + public async Task Should_return_schema_from_id_if_string_is_guid() + { + SetupSchemaFound(); + + var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_return_schema_from_name_if_string_not_guid() + { + SetupSchemaFound(); + + var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_throw_404_if_schema_not_found() + { + SetupSchemaNotFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); + } + + [Fact] + public async Task Should_throw_404_if_schema_not_found_in_check() + { + SetupSchemaNotFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); + } + + [Fact] + public async Task Should_throw_for_single_content_if_no_permission() + { + SetupUser(false, false); + SetupSchemaFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.FindContentAsync(ctx, schemaId.Name, contentId)); + } + + [Fact] + public async Task Should_throw_404_for_single_content_if_content_not_found() + { + var status = new[] { Status.Published }; + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupContent(status, null, includeDraft: false); + + var ctx = requestContext; + + await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, schemaId.Name, contentId)); + } + + [Fact] + public async Task Should_return_single_content_for_frontend_without_transform() + { + var content = CreateContent(contentId); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContent(null, content, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); + + Assert.Equal(contentTransformed, result!.Data); + Assert.Equal(content.Id, result.Id); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_return_single_content_for_api_with_transform(int unpublished, Status[] status) + { + var content = CreateContent(contentId); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContent(status, content, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); + + Assert.Equal(contentTransformed, result!.Data); + Assert.Equal(content.Id, result.Id); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_return_versioned_content_from_repository_and_transform() + { + var content = CreateContent(contentId); + + SetupUser(true); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + + A.CallTo(() => contentVersionLoader.GetAsync(contentId, 10)) + .Returns(content); + + var ctx = requestContext; + + var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId, 10); + + Assert.Equal(contentTransformed, result!.Data); + Assert.Equal(content.Id, result.Id); + } + + [Fact] + public async Task Should_throw_for_query_if_no_permission() + { + SetupUser(false, false); + SetupSchemaFound(); + + var ctx = requestContext; + + await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty)); + } + + [Fact] + public async Task Should_query_contents_by_query_for_frontend_without_transform() + { + const int count = 5, total = 200; + + var content = CreateContent(contentId); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContents(null, count, total, content, inDraft: true, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + + Assert.Equal(contentData, result[0].Data); + Assert.Equal(content.Id, result[0].Id); + + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_query_contents_by_query_for_api_and_transform(int unpublished, Status[] status) + { + const int count = 5, total = 200; + + var content = CreateContent(contentId); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(contentId); + SetupContents(status, count, total, content, inDraft: false, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + + Assert.Equal(contentData, result[0].Data); + Assert.Equal(contentId, result[0].Id); + + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_query_contents_by_id_for_frontend_and_transform() + { + const int count = 5, total = 200; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(null, total, ids, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_query_contents_by_id_for_api_and_transform(int unpublished, Status[] status) + { + const int count = 5, total = 200; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(status, total, ids, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + Assert.Equal(total, result.Total); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_query_all_contents_by_id_for_frontend_and_transform() + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: true); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(null, ids, includeDraft: true); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(ApiStatusTests))] + public async Task Should_query_all_contents_by_id_for_api_and_transform(int unpublished, Status[] status) + { + const int count = 5; + + var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: false); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(status, ids, unpublished == 1); + + var ctx = requestContext.WithUnpublished(unpublished == 1); + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Equal(ids, result.Select(x => x.Id).ToList()); + + A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) + .MustHaveHappened(count, Times.Exactly); + } + + [Fact] + public async Task Should_skip_contents_when_user_has_no_permission() + { + var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); + + SetupUser(isFrontend: false, allowSchema: false); + SetupSchemaFound(); + SetupSchemaScripting(ids.ToArray()); + SetupContents(new Status[0], ids, includeDraft: false); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_not_call_repository_if_no_id_defined() + { + var ids = new List(); + + SetupUser(isFrontend: false, allowSchema: false); + SetupSchemaFound(); + + var ctx = requestContext; + + var result = await sut.QueryAsync(ctx, ids); + + Assert.Empty(result); + + A.CallTo(() => contentRepository.QueryAsync(app, A.Ignored, A>.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private void SetupUser(bool isFrontend, bool allowSchema = true) + { + if (isFrontend) + { + identity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + } + + if (allowSchema) + { + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.ForApp(Permissions.AppContentsRead, app.Name, schema.SchemaDef.Name).Id)); + } + + requestContext.UpdatePermissions(); + } + + private void SetupSchemaScripting(params Guid[] ids) + { + foreach (var id in ids) + { + A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), "")) + .Returns(contentTransformed); + } + } + + private void SetupContents(Status[]? status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft) + { + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), inDraft, A.Ignored, includeDraft)) + .Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); + } + + private void SetupContents(Status[]? status, int total, List ids, bool includeDraft) + { + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), A>.Ignored, includeDraft)) + .Returns(ResultList.Create(total, ids.Select(CreateContent).Shuffle())); + } + + private void SetupContents(Status[]? status, List ids, bool includeDraft) + { + A.CallTo(() => contentRepository.QueryAsync(app, A.That.Is(status), A>.Ignored, includeDraft)) + .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); + } + + private void SetupContent(Status[]? status, IContentEntity? content, bool includeDraft) + { + A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(status), contentId, includeDraft)) + .Returns(content); + } + + private void SetupSchemaFound() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) + .Returns(schema); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + } + + private void SetupSchemaNotFound() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) + .Returns((ISchemaEntity?)null); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns((ISchemaEntity?)null); + } + + private void SetupEnricher() + { + A.CallTo(() => contentEnricher.EnrichAsync(A>.Ignored, requestContext)) + .ReturnsLazily(x => + { + var input = (IEnumerable)x.Arguments[0]; + + return Task.FromResult>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList()); + }); + } + + private IContentEntity CreateContent(Guid id) + { + return CreateContent(id, Status.Published); + } + + private IContentEntity CreateContent(Guid id, Status status) + { + var content = new ContentEntity + { + Id = id, + Data = contentData, + DataDraft = contentData, + SchemaId = schemaId, + Status = status + }; + + return content; + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs new file mode 100644 index 000000000..05ee89c0a --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class FilterTagTransformerTests + { + private readonly ITagService tagService = A.Fake(); + private readonly ISchemaEntity schema; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + + public FilterTagTransformerTests() + { + var schemaDef = + new Schema("schema") + .AddTags(1, "tags1", Partitioning.Invariant) + .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) + .AddString(3, "string", Partitioning.Invariant); + + schema = Mocks.Schema(appId, schemaId, schemaDef); + } + + [Fact] + public void Should_normalize_tags() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A>.That.Contains("name1"))) + .Returns(new Dictionary { ["name1"] = "id1" }); + + var source = ClrFilter.Eq("data.tags2.iv", "name1"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.tags2.iv == 'id1'", result!.ToString()); + } + + [Fact] + public void Should_not_fail_when_tags_not_found() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"))) + .Returns(new Dictionary()); + + var source = ClrFilter.Eq("data.tags2.iv", "name1"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.tags2.iv == 'name1'", result!.ToString()); + } + + [Fact] + public void Should_not_normalize_other_tags_field() + { + var source = ClrFilter.Eq("data.tags1.iv", "value"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.tags1.iv == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public void Should_not_normalize_other_typed_field() + { + var source = ClrFilter.Eq("data.string.iv", "value"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("data.string.iv == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public void Should_not_normalize_non_data_field() + { + var source = ClrFilter.Eq("no.data", "value"); + + var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); + + Assert.Equal("no.data == 'value'", result!.ToString()); + + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs new file mode 100644 index 000000000..f370f6190 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs @@ -0,0 +1,263 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public class TextIndexerGrainTests : IDisposable + { + private readonly Guid schemaId = Guid.NewGuid(); + private readonly List ids1 = new List { Guid.NewGuid() }; + private readonly List ids2 = new List { Guid.NewGuid() }; + private readonly SearchContext context; + private readonly IAssetStore assetStore = new MemoryAssetStore(); + private readonly TextIndexerGrain sut; + + public TextIndexerGrainTests() + { + context = new SearchContext + { + Languages = new HashSet { "de", "en" } + }; + + sut = new TextIndexerGrain(assetStore); + sut.ActivateAsync(schemaId).Wait(); + } + + public void Dispose() + { + sut.OnDeactivateAsync().Wait(); + } + + [Fact] + public async Task Should_throw_exception_for_invalid_query() + { + await Assert.ThrowsAsync(() => sut.SearchAsync("~hello", context)); + } + + [Fact] + public async Task Should_read_index_and_retrieve() + { + await AddInvariantContent("Hello", "World", false); + + await sut.DeactivateAsync(true); + + var other = new TextIndexerGrain(assetStore); + try + { + await other.ActivateAsync(schemaId); + + await TestSearchAsync(ids1, "Hello", grain: other); + await TestSearchAsync(ids2, "World", grain: other); + } + finally + { + await other.OnDeactivateAsync(); + } + } + + [Fact] + public async Task Should_index_invariant_content_and_retrieve() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "Hello"); + await TestSearchAsync(ids2, "World"); + } + + [Fact] + public async Task Should_index_invariant_content_and_retrieve_with_fuzzy() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "helo~"); + await TestSearchAsync(ids2, "wold~"); + } + + [Fact] + public async Task Should_update_draft_only() + { + await AddInvariantContent("Hello", "World", false); + await AddInvariantContent("Hallo", "Welt", false); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(null, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Hallo", Scope.Draft); + await TestSearchAsync(null, "Hallo", Scope.Published); + } + + [Fact] + public async Task Should_also_update_published_after_copy() + { + await AddInvariantContent("Hello", "World", false); + + await CopyAsync(true); + + await AddInvariantContent("Hallo", "Welt", false); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(null, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Hallo", Scope.Draft); + await TestSearchAsync(ids1, "Hallo", Scope.Published); + } + + [Fact] + public async Task Should_simulate_content_reversion() + { + await AddInvariantContent("Hello", "World", false); + + await CopyAsync(true); + + await AddInvariantContent("Hallo", "Welt", true); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Hallo", Scope.Draft); + await TestSearchAsync(null, "Hallo", Scope.Published); + + await CopyAsync(false); + + await TestSearchAsync(ids1, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + + await TestSearchAsync(null, "Hallo", Scope.Draft); + await TestSearchAsync(null, "Hallo", Scope.Published); + + await AddInvariantContent("Guten Morgen", "Welt", true); + + await TestSearchAsync(null, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + + await TestSearchAsync(ids1, "Guten Morgen", Scope.Draft); + await TestSearchAsync(null, "Guten Morgen", Scope.Published); + } + + [Fact] + public async Task Should_also_retrieve_published_content_after_copy() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "Hello", Scope.Draft); + await TestSearchAsync(null, "Hello", Scope.Published); + + await CopyAsync(true); + + await TestSearchAsync(ids1, "Hello", Scope.Draft); + await TestSearchAsync(ids1, "Hello", Scope.Published); + } + + [Fact] + public async Task Should_delete_documents_from_index() + { + await AddInvariantContent("Hello", "World", false); + + await TestSearchAsync(ids1, "Hello"); + await TestSearchAsync(ids2, "World"); + + await DeleteAsync(ids1[0]); + + await TestSearchAsync(null, "Hello"); + await TestSearchAsync(ids2, "World"); + } + + [Fact] + public async Task Should_search_by_field() + { + await AddLocalizedContent(); + + await TestSearchAsync(null, "de:city"); + await TestSearchAsync(null, "en:Stadt"); + } + + [Fact] + public async Task Should_index_localized_content_and_retrieve() + { + await AddLocalizedContent(); + + await TestSearchAsync(ids1, "Stadt"); + await TestSearchAsync(ids1, "and"); + await TestSearchAsync(ids2, "und"); + + await TestSearchAsync(ids2, "City"); + await TestSearchAsync(ids2, "und"); + await TestSearchAsync(ids1, "and"); + } + + private async Task AddLocalizedContent() + { + var germanData = + new NamedContentData() + .AddField("localized", + new ContentFieldData() + .AddValue("de", "Stadt und Umgebung and whatever")); + + var englishData = + new NamedContentData() + .AddField("localized", + new ContentFieldData() + .AddValue("en", "City and Surroundings und sonstiges")); + + await sut.IndexAsync(new Update { Id = ids1[0], Data = germanData, OnlyDraft = true }); + await sut.IndexAsync(new Update { Id = ids2[0], Data = englishData, OnlyDraft = true }); + } + + private async Task AddInvariantContent(string text1, string text2, bool onlyDraft = false) + { + var data1 = + new NamedContentData() + .AddField("test", + new ContentFieldData() + .AddValue("iv", text1)); + + var data2 = + new NamedContentData() + .AddField("test", + new ContentFieldData() + .AddValue("iv", text2)); + + await sut.IndexAsync(new Update { Id = ids1[0], Data = data1, OnlyDraft = onlyDraft }); + await sut.IndexAsync(new Update { Id = ids2[0], Data = data2, OnlyDraft = onlyDraft }); + } + + private async Task DeleteAsync(Guid id) + { + await sut.DeleteAsync(id); + } + + private async Task CopyAsync(bool fromDraft) + { + await sut.CopyAsync(ids1[0], fromDraft); + await sut.CopyAsync(ids2[0], fromDraft); + } + + private async Task TestSearchAsync(List? expected, string text, Scope target = Scope.Draft, TextIndexerGrain? grain = null) + { + context.Scope = target; + + var result = await (grain ?? sut).SearchAsync(text, context); + + if (expected != null) + { + Assert.Equal(expected, result); + } + else + { + Assert.Empty(result); + } + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs new file mode 100644 index 000000000..be219adaa --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs @@ -0,0 +1,191 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.History.Notifications +{ + public class NotificationEmailEventConsumerTests + { + private readonly INotificationEmailSender emailSender = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly IUser assigner = A.Fake(); + private readonly IUser assignee = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly string assignerId = Guid.NewGuid().ToString(); + private readonly string assigneeId = Guid.NewGuid().ToString(); + private readonly string appName = "my-app"; + private readonly NotificationEmailEventConsumer sut; + + public NotificationEmailEventConsumerTests() + { + A.CallTo(() => emailSender.IsActive) + .Returns(true); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(assigner); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(assignee); + + sut = new NotificationEmailEventConsumer(emailSender, userResolver, log); + } + + [Fact] + public async Task Should_not_send_email_if_contributors_assigned_by_clients() + { + var @event = CreateEvent(RefTokenType.Client, true); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_for_initial_owner() + { + var @event = CreateEvent(RefTokenType.Subject, false, streamNumber: 1); + + await sut.On(@event); + + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_for_old_events() + { + var @event = CreateEvent(RefTokenType.Subject, true, instant: SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50))); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_for_old_contributor() + { + var @event = CreateEvent(RefTokenType.Subject, true, isNewContributor: false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_if_sender_not_active() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + A.CallTo(() => emailSender.IsActive) + .Returns(false); + + await sut.On(@event); + + MustNotResolveUser(); + MustNotSendEmail(); + } + + [Fact] + public async Task Should_not_send_email_if_assigner_not_found() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_email_if_assignee_not_found() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + MustNotSendEmail(); + MustLogWarning(); + } + + [Fact] + public async Task Should_send_email_for_new_user() + { + var @event = CreateEvent(RefTokenType.Subject, true); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, true)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_send_email_for_existing_user() + { + var @event = CreateEvent(RefTokenType.Subject, false); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, false)) + .MustHaveHappened(); + } + + private void MustLogWarning() + { + A.CallTo(() => log.Log(SemanticLogLevel.Warning, A.Ignored, A>.Ignored)) + .MustHaveHappened(); + } + + private void MustNotResolveUser() + { + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + private void MustNotSendEmail() + { + A.CallTo(() => emailSender.SendContributorEmailAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private Envelope CreateEvent(string assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + { + var @event = new AppContributorAssigned + { + Actor = new RefToken(assignerType, assignerId), + AppId = NamedId.Of(Guid.NewGuid(), appName), + ContributorId = assigneeId, + IsCreated = isNewUser, + IsAdded = isNewContributor + }; + + var envelope = Envelope.Create(@event); + + envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); + envelope.SetEventStreamNumber(streamNumber); + + return envelope; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailSenderTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs new file mode 100644 index 000000000..8883862db --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -0,0 +1,188 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public class GuardRuleTests + { + private readonly Uri validUrl = new Uri("https://squidex.io"); + private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction()).Rename("MyName"); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly IAppProvider appProvider = A.Fake(); + + public sealed class TestAction : RuleAction + { + public Uri Url { get; set; } + } + + public GuardRuleTests() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(Mocks.Schema(appId, schemaId)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_trigger_null() + { + var command = CreateCommand(new CreateRule + { + Trigger = null!, + Action = new TestAction + { + Url = validUrl + } + }); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), + new ValidationError("Trigger is required.", "Trigger")); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_action_null() + { + var command = CreateCommand(new CreateRule + { + Trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }, + Action = null! + }); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), + new ValidationError("Action is required.", "Action")); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = CreateCommand(new CreateRule + { + Trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }, + Action = new TestAction + { + Url = validUrl + } + }); + + await GuardRule.CanCreate(command, appProvider); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() + { + var command = new UpdateRule(); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), + new ValidationError("Either trigger, action or name is required.", "Trigger", "Action")); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_rule_has_already_this_name() + { + var command = new UpdateRule + { + Name = "MyName" + }; + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), + new ValidationError("Rule has already this name.", "Name")); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_trigger_action__and_name_are_valid() + { + var command = new UpdateRule + { + Trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }, + Action = new TestAction + { + Url = validUrl + }, + Name = "NewName" + }; + + await GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0); + } + + [Fact] + public void CanEnable_should_throw_exception_if_rule_enabled() + { + var command = new EnableRule(); + + var rule_1 = rule_0.Enable(); + + Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_rule_disabled() + { + var command = new EnableRule(); + + var rule_1 = rule_0.Disable(); + + GuardRule.CanEnable(command, rule_1); + } + + [Fact] + public void CanDisable_should_throw_exception_if_rule_disabled() + { + var command = new DisableRule(); + + var rule_1 = rule_0.Disable(); + + Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); + } + + [Fact] + public void CanDisable_should_not_throw_exception_if_rule_enabled() + { + var command = new DisableRule(); + + var rule_1 = rule_0.Enable(); + + GuardRule.CanDisable(command, rule_1); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteRule(); + + GuardRule.CanDelete(command); + } + + private CreateRule CreateCommand(CreateRule command) + { + command.AppId = appId; + + return command; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs new file mode 100644 index 000000000..bd76b46bb --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers +{ + public class ContentChangedTriggerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + + [Fact] + public async Task Should_add_error_if_schema_id_is_not_defined() + { + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2()) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Schema id is required.", "Schemas") + }); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(Task.FromResult(null)); + + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError($"Schema {schemaId.Id} does not exist.", "Schemas") + }); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_null() + { + var trigger = new ContentChangedTriggerV2(); + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_empty() + { + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Empty() + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_ids_are_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .Returns(Mocks.Schema(appId, schemaId)); + + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs new file mode 100644 index 000000000..2fcc13a29 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class ManualTriggerHandlerTests + { + private readonly IRuleTriggerHandler sut = new ManualTriggerHandler(); + + [Fact] + public async Task Should_create_event_with_name() + { + var envelope = Envelope.Create(new RuleManuallyTriggered()); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal("Manual", result!.Name); + } + + [Fact] + public void Should_always_trigger() + { + Assert.True(sut.Trigger(new EnrichedManualEvent(), new ManualTrigger())); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs new file mode 100644 index 000000000..9eef2daa6 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleEnqueuerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly RuleService ruleService = A.Fake(); + private readonly RuleEnqueuer sut; + + public sealed class TestAction : RuleAction + { + public Uri Url { get; set; } + } + + public RuleEnqueuerTests() + { + sut = new RuleEnqueuer( + appProvider, + cache, + ruleEventRepository, + ruleService); + } + + [Fact] + public void Should_return_contents_filter_for_events_filter() + { + Assert.Equal(".*", sut.EventsFilter); + } + + [Fact] + public void Should_return_type_name_for_name() + { + Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); + } + + [Fact] + public async Task Should_do_nothing_on_clear() + { + await sut.ClearAsync(); + } + + [Fact] + public async Task Should_update_repository_when_enqueing() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule = CreateRule(); + + var job = new RuleJob { Created = now }; + + A.CallTo(() => ruleService.CreateJobAsync(rule.RuleDef, rule.Id, @event)) + .Returns(job); + + await sut.Enqueue(rule.RuleDef, rule.Id, @event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_update_repositories_with_jobs_from_service() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule1 = CreateRule(); + var rule2 = CreateRule(); + + var job1 = new RuleJob { Created = now }; + + A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) + .Returns(new List { rule1, rule2 }); + + A.CallTo(() => ruleService.CreateJobAsync(rule1.RuleDef, rule1.Id, @event)) + .Returns(job1); + + A.CallTo(() => ruleService.CreateJobAsync(rule2.RuleDef, rule2.Id, @event)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) + .MustHaveHappened(); + } + + private static RuleEntity CreateRule() + { + var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); + + return new RuleEntity { RuleDef = rule, Id = Guid.NewGuid() }; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs new file mode 100644 index 000000000..7be5758ec --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public class UsageTriggerHandlerTests + { + private readonly Guid ruleId = Guid.NewGuid(); + private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + var result = sut.Trigger(new ContentCreated(), new UsageTrigger(), ruleId); + + Assert.False(result); + } + + [Fact] + public void Should_not_trigger_precheck_when_rule_id_not_matchs() + { + var result = sut.Trigger(new AppUsageExceeded { RuleId = Guid.NewGuid() }, new UsageTrigger(), ruleId); + + Assert.True(result); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct_and_rule_id_matchs() + { + var result = sut.Trigger(new AppUsageExceeded { RuleId = ruleId }, new UsageTrigger(), ruleId); + + Assert.True(result); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); + + Assert.False(result); + } + + [Fact] + public async Task Should_create_enriched_event() + { + var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; + + var result = await sut.CreateEnrichedEventAsync(Envelope.Create(@event)) as EnrichedUsageExceededEvent; + + Assert.Equal(@event.CallsCurrent, result!.CallsCurrent); + Assert.Equal(@event.CallsLimit, result!.CallsLimit); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ArrayFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs new file mode 100644 index 000000000..73c01dddc --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs @@ -0,0 +1,379 @@ +// ========================================================================== +// 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.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public class GuardSchemaFieldTests + { + private readonly Schema schema_0; + private readonly StringFieldProperties validProperties = new StringFieldProperties(); + private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; + + public GuardSchemaFieldTests() + { + schema_0 = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant) + .AddString(2, "field2", Partitioning.Invariant) + .AddArray(3, "field3", Partitioning.Invariant, f => f + .AddNumber(301, "field301")) + .AddUI(4, "field4", Partitioning.Invariant); + } + + private static Action A(Action method) where T : FieldCommand + { + return method; + } + + private static Func S(Func method) + { + return method; + } + + public static IEnumerable FieldCommandData = new[] + { + new object[] { A(GuardSchemaField.CanEnable) }, + new object[] { A(GuardSchemaField.CanDelete) }, + new object[] { A(GuardSchemaField.CanDisable) }, + new object[] { A(GuardSchemaField.CanHide) }, + new object[] { A(GuardSchemaField.CanLock) }, + new object[] { A(GuardSchemaField.CanShow) }, + new object[] { A(GuardSchemaField.CanUpdate) } + }; + + public static IEnumerable InvalidStates = new[] + { + new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(1)) }, + new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(1)) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s.LockField(1)) }, + new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(1)) } + }; + + public static IEnumerable InvalidNestedStates = new[] + { + new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(301, 3)) }, + new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(301, 3)) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s) }, + new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(301, 3)) } + }; + + public static IEnumerable ValidStates = new[] + { + new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, + new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(1)) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(1)) } + }; + + public static IEnumerable ValidNestedStates = new[] + { + new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(301, 3)) }, + new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, + new object[] { A(GuardSchemaField.CanHide), S(s => s) }, + new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(301, 3)) } + }; + + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_field_not_found(Action action) where T : FieldCommand, new() + { + var command = new T { FieldId = 5 }; + + Assert.Throws(() => action(schema_0, command)); + } + + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_parent_field_not_found(Action action) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 4, FieldId = 401 }; + + Assert.Throws(() => action(schema_0, command)); + } + + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_child_field_not_found(Action action) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 302 }; + + Assert.Throws(() => action(schema_0, command)); + } + + [Theory] + [MemberData(nameof(InvalidStates))] + public void Commands_should_throw_exception_if_state_not_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { FieldId = 1 }; + + Assert.Throws(() => action(updater(schema_0), command)); + } + + [Theory] + [MemberData(nameof(InvalidNestedStates))] + public void Commands_should_throw_exception_if_nested_state_not_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 301 }; + + Assert.Throws(() => action(updater(schema_0), command)); + } + + [Theory] + [MemberData(nameof(ValidStates))] + public void Commands_should_not_throw_exception_if_state_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { FieldId = 1 }; + + action(updater(schema_0), command); + } + + [Theory] + [MemberData(nameof(ValidNestedStates))] + public void Commands_should_not_throw_exception_if_nested_state_valid(Action action, Func updater) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 301 }; + + action(updater(schema_0), command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_locked() + { + var command = new DeleteField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); + } + + [Fact] + public void CanDisable_should_throw_exception_if_already_disabled() + { + var command = new DisableField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Disable()); + + Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); + } + + [Fact] + public void CanDisable_should_throw_exception_if_ui_field() + { + var command = new DisableField { FieldId = 4 }; + + Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); + } + + [Fact] + public void CanEnable_should_throw_exception_if_already_enabled() + { + var command = new EnableField { FieldId = 1 }; + + Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_locked() + { + var command = new HideField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_already_hidden() + { + var command = new HideField { FieldId = 1 }; + + var schema_1 = schema_0.UpdateField(1, f => f.Hide()); + + Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_ui_field() + { + var command = new HideField { FieldId = 4 }; + + Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); + } + + [Fact] + public void CanShow_should_throw_exception_if_already_visible() + { + var command = new ShowField { FieldId = 4 }; + + Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_not_locked() + { + var command = new DeleteField { FieldId = 1 }; + + GuardSchemaField.CanDelete(schema_0, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_locked() + { + var command = new UpdateField { FieldId = 1, Properties = validProperties }; + + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + + Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_not_locked() + { + var command = new UpdateField { FieldId = 1, Properties = validProperties }; + + GuardSchemaField.CanUpdate(schema_0, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_list_field() + { + var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsListField = true } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_reference_field() + { + var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsReferenceField = true } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("UI field cannot be a reference field.", "Properties.IsReferenceField")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_properties_null() + { + var command = new UpdateField { FieldId = 2, Properties = null! }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("Properties is required.", "Properties")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_properties_not_valid() + { + var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), + new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_field_already_exists() + { + var command = new AddField { Name = "field1", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("A field with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_nested_field_already_exists() + { + var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("A field with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_not_valid() + { + var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Name must be a valid javascript property name.", "Name")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_not_valid() + { + var command = new AddField { Name = "field5", Properties = invalidProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_null() + { + var command = new AddField { Name = "field5", Properties = null! }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Properties is required.", "Properties")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_partitioning_not_valid() + { + var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("Partitioning is not a valid value.", "Partitioning")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_creating_a_ui_field_as_list_field() + { + var command = new AddField { Name = "field5", Properties = new UIFieldProperties { IsListField = true } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), + new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_parent_not_exists() + { + var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_not_exists() + { + var command = new AddField { Name = "field5", Properties = validProperties }; + + GuardSchemaField.CanAdd(schema_0, command); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_exists_in_root() + { + var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; + + GuardSchemaField.CanAdd(schema_0, command); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs new file mode 100644 index 000000000..3e7deb2d8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -0,0 +1,530 @@ +// ========================================================================== +// 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.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public class GuardSchemaTests + { + private readonly Schema schema_0; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + + public GuardSchemaTests() + { + schema_0 = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant) + .AddString(2, "field2", Partitioning.Invariant); + } + + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Name is not a valid slug.", "Name")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_name_invalid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "invalid name", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field name must be a valid javascript property name.", + "Fields[1].Name")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_properties_null() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = null!, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field properties is required.", + "Fields[1].Properties")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_properties_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Max length must be greater or equal to min length.", + "Fields[1].Properties.MinLength", + "Fields[1].Properties.MaxLength")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = "INVALID" + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Partitioning is not a valid value.", + "Fields[1].Partitioning")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Fields cannot have duplicate names.", + "Fields")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_name_invalid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "invalid name", + Properties = new StringFieldProperties() + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field name must be a valid javascript property name.", + "Fields[1].Nested[1].Name")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_properties_null() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = null! + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field properties is required.", + "Fields[1].Nested[1].Properties")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_is_array() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new ArrayFieldProperties() + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Nested field cannot be array fields.", + "Fields[1].Nested[1].Properties")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Max length must be greater or equal to min length.", + "Fields[1].Nested[1].Properties.MinLength", + "Fields[1].Nested[1].Properties.MaxLength")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties() + }, + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties() + } + } + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Fields cannot have duplicate names.", + "Fields[1].Nested")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_ui_field_is_invalid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new UIFieldProperties + { + IsListField = true, + IsReferenceField = true + }, + IsHidden = true, + IsDisabled = true, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }; + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("UI field cannot be a list field.", + "Fields[1].Properties.IsListField"), + new ValidationError("UI field cannot be a reference field.", + "Fields[1].Properties.IsReferenceField"), + new ValidationError("UI field cannot be hidden.", + "Fields[1].IsHidden"), + new ValidationError("UI field cannot be disabled.", + "Fields[1].IsDisabled")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_command_is_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties + { + IsListField = true + }, + IsHidden = true, + IsDisabled = true, + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field2", + Properties = ValidProperties(), + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field3", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new List + { + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = ValidProperties() + }, + new UpsertSchemaNestedField + { + Name = "nested2", + Properties = ValidProperties() + } + } + } + }, + Name = "new-schema" + }; + + GuardSchema.CanCreate(command); + } + + [Fact] + public void CanPublish_should_throw_exception_if_already_published() + { + var command = new PublishSchema(); + + var schema_1 = schema_0.Publish(); + + Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); + } + + [Fact] + public void CanPublish_should_not_throw_exception_if_not_published() + { + var command = new PublishSchema(); + + GuardSchema.CanPublish(schema_0, command); + } + + [Fact] + public void CanUnpublish_should_throw_exception_if_already_unpublished() + { + var command = new UnpublishSchema(); + + Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); + } + + [Fact] + public void CanUnpublish_should_not_throw_exception_if_already_published() + { + var command = new UnpublishSchema(); + + var schema_1 = schema_0.Publish(); + + GuardSchema.CanUnpublish(schema_1, command); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() + { + var command = new ReorderFields { FieldIds = new List { 1, 3 } }; + + ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + new ValidationError("Field ids do not cover all fields.", "FieldIds")); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() + { + var command = new ReorderFields { FieldIds = new List { 1 } }; + + ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + new ValidationError("Field ids do not cover all fields.", "FieldIds")); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_null() + { + var command = new ReorderFields { FieldIds = null! }; + + ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), + new ValidationError("Field ids is required.", "FieldIds")); + } + + [Fact] + public void CanReorder_should_throw_exception_if_parent_field_not_found() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 }, ParentFieldId = 99 }; + + Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); + } + + [Fact] + public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 } }; + + GuardSchema.CanReorder(schema_0, command); + } + + [Fact] + public void CanConfigurePreviewUrls_should_throw_exception_if_preview_urls_null() + { + var command = new ConfigurePreviewUrls { PreviewUrls = null! }; + + ValidationAssert.Throws(() => GuardSchema.CanConfigurePreviewUrls(command), + new ValidationError("Preview Urls is required.", "PreviewUrls")); + } + + [Fact] + public void CanConfigurePreviewUrls_should_not_throw_exception_if_valid() + { + var command = new ConfigurePreviewUrls { PreviewUrls = new Dictionary() }; + + GuardSchema.CanConfigurePreviewUrls(command); + } + + [Fact] + public void CanChangeCategory_should_not_throw_exception() + { + var command = new ChangeCategory(); + + GuardSchema.CanChangeCategory(schema_0, command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteSchema(); + + GuardSchema.CanDelete(schema_0, command); + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs new file mode 100644 index 000000000..5befc397b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -0,0 +1,248 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public class SchemasIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly ISchemasByAppIndexGrain index = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly SchemasIndex sut; + + public SchemasIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(index); + + sut = new SchemasIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_schema_by_id() + { + var schema = SetupSchema(0, false); + + var actual = await sut.GetSchemaAsync(appId.Id, schema.Id); + + Assert.Same(actual, schema); + } + + [Fact] + public async Task Should_resolve_schema_by_name() + { + var schema = SetupSchema(0, false); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemaByNameAsync(appId.Id, schema.SchemaDef.Name); + + Assert.Same(actual, schema); + } + + [Fact] + public async Task Should_resolve_schemas_by_id() + { + var schema = SetupSchema(0, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { schema.Id }); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Same(actual[0], schema); + } + + [Fact] + public async Task Should_return_empty_schema_if_schema_not_created() + { + var schema = SetupSchema(-1, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { schema.Id }); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_return_empty_schema_if_schema_deleted() + { + var schema = SetupSchema(0, true); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_also_return_schema_if_deleted_allowed() + { + var schema = SetupSchema(-1, true); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemasAsync(appId.Id, true); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_add_schema_to_index_on_create() + { + var token = RandomHash.Simple(); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(schemaId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_clear_reservation_when_schema_creation_failed() + { + var token = RandomHash.Simple(); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(schemaId.Name), commandBus); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(token)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(token)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_index_on_create_if_name_taken() + { + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(Task.FromResult(null)); + + var context = + new CommandContext(Create(schemaId.Name), commandBus) + .Complete(); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => index.AddAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_index_on_create_if_name_invalid() + { + var context = + new CommandContext(Create("INVALID"), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_remove_schema_from_index_on_delete() + { + var schema = SetupSchema(0, false); + + var context = + new CommandContext(new DeleteSchema { SchemaId = schema.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAsync(schema.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var schemas = new Dictionary(); + + await sut.RebuildAsync(appId.Id, schemas); + + A.CallTo(() => index.RebuildAsync(schemas)) + .MustHaveHappened(); + } + + private CreateSchema Create(string name) + { + return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; + } + + private ISchemaEntity SetupSchema(long version, bool deleted) + { + var schemaEntity = A.Fake(); + + A.CallTo(() => schemaEntity.SchemaDef) + .Returns(new Schema(schemaId.Name)); + A.CallTo(() => schemaEntity.Id) + .Returns(schemaId.Id); + A.CallTo(() => schemaEntity.AppId) + .Returns(appId); + A.CallTo(() => schemaEntity.Version) + .Returns(version); + A.CallTo(() => schemaEntity.IsDeleted) + .Returns(deleted); + + var schemaGrain = A.Fake(); + + A.CallTo(() => schemaGrain.GetStateAsync()) + .Returns(J.Of(schemaEntity)); + + A.CallTo(() => grainFactory.GetGrain(schemaId.Id, null)) + .Returns(schemaGrain); + + return schemaEntity; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..dbbd928b5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IRuleTriggerHandler sut; + + public SchemaChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new SchemaChangedTriggerHandler(scriptEngine); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }, + new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }, + new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }, + new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }, + new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(SchemaEvent @event, EnrichedSchemaEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal(type, ((EnrichedSchemaEvent)result!).Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new AppCreated(), trigger, Guid.NewGuid()); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new SchemaCreated(), trigger, Guid.NewGuid()); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedContentEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForCondition("true", trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForCondition("false", trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.False(result); + }); + } + + private void TestForCondition(string condition, Action action) + { + var trigger = new SchemaChangedTrigger { Condition = condition }; + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj new file mode 100644 index 000000000..81e4d6ec5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -0,0 +1,40 @@ + + + Exe + netcoreapp3.0 + Squidex.Domain.Apps.Entities + 8.0 + enable + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs new file mode 100644 index 000000000..1fbb559f4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class AExtensions + { + public static ClrQuery Is(this INegatableArgumentConstraintManager that, string query) + { + return that.Matches(x => x.ToString() == query); + } + + public static T[] Is(this INegatableArgumentConstraintManager that, params T[]? values) + { + if (values == null) + { + return that.IsNull(); + } + + return that.IsSameSequenceAs(values); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs new file mode 100644 index 000000000..01871cca9 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class JsonHelper + { + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + + public static IJsonSerializer CreateSerializer(TypeNameRegistry? typeNameRegistry = null) + { + var serializerSettings = DefaultSettings(typeNameRegistry); + + return new NewtonsoftJsonSerializer(serializerSettings); + } + + public static JsonSerializerSettings DefaultSettings(TypeNameRegistry? typeNameRegistry = null) + { + return new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), + + ContractResolver = new ConverterContractResolver( + new ClaimsPrincipalConverter(), + new InstantConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new StringEnumConverter()), + + TypeNameHandling = TypeNameHandling.Auto + }; + } + + public static T SerializeAndDeserialize(this T value) + { + return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; + } + + public static T Deserialize(string value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; + } + + public static T Deserialize(object value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs new file mode 100644 index 000000000..ad363bb6b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class Mocks + { + public static IAppEntity App(NamedId appId, params Language[] languages) + { + var config = LanguagesConfig.English; + + foreach (var language in languages) + { + config = config.Set(language); + } + + var app = A.Fake(); + + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + A.CallTo(() => app.LanguagesConfig).Returns(config); + + return app; + } + + public static ISchemaEntity Schema(NamedId appId, NamedId schemaId, Schema? schemaDef = null) + { + var schema = A.Fake(); + + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef ?? new Schema(schemaId.Name)); + + return schema; + } + + public static ClaimsPrincipal ApiUser(string? role = null) + { + return CreateUser(role, "api"); + } + + public static ClaimsPrincipal FrontendUser(string? role = null) + { + return CreateUser(role, DefaultClients.Frontend); + } + + private static ClaimsPrincipal CreateUser(string? role, string client) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); + + if (role != null) + { + claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); + } + + return claimsPrincipal; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs similarity index 100% rename from tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs diff --git a/tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs similarity index 100% rename from tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs rename to backend/tests/Squidex.Domain.Users.Tests/AssetUserPictureStoreTests.cs diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs new file mode 100644 index 000000000..ecb332377 --- /dev/null +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.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.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Squidex.Domain.Users +{ + public class DefaultUserResolverTests + { + private readonly UserManager userManager = A.Fake>(); + private readonly DefaultUserResolver sut; + + public DefaultUserResolverTests() + { + var userFactory = A.Fake(); + + A.CallTo(() => userFactory.IsId(A.That.StartsWith("id"))) + .Returns(true); + + A.CallTo(() => userManager.NormalizeEmail(A.Ignored)) + .ReturnsLazily(c => c.GetArgument(0).ToUpperInvariant()); + + var serviceProvider = A.Fake(); + + var scope = A.Fake(); + + var scopeFactory = A.Fake(); + + A.CallTo(() => scopeFactory.CreateScope()) + .Returns(scope); + + A.CallTo(() => scope.ServiceProvider) + .Returns(serviceProvider); + + A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))) + .Returns(scopeFactory); + + A.CallTo(() => serviceProvider.GetService(typeof(IUserFactory))) + .Returns(userFactory); + + A.CallTo(() => serviceProvider.GetService(typeof(UserManager))) + .Returns(userManager); + + sut = new DefaultUserResolver(serviceProvider); + } + + [Fact] + public async Task Should_resolve_user_by_email() + { + var (user, claims) = GernerateUser("id1"); + + A.CallTo(() => userManager.FindByEmailAsync(user.Email)) + .Returns(user); + + A.CallTo(() => userManager.GetClaimsAsync(user)) + .Returns(claims); + + var result = await sut.FindByIdOrEmailAsync(user.Email); + + Assert.Equal(user.Id, result!.Id); + Assert.Equal(user.Email, result!.Email); + + Assert.Equal(claims, result!.Claims); + } + + [Fact] + public async Task Should_resolve_user_by_id1() + { + var (user, claims) = GernerateUser("id2"); + + A.CallTo(() => userManager.FindByIdAsync(user.Id)) + .Returns(user); + + A.CallTo(() => userManager.GetClaimsAsync(user)) + .Returns(claims); + + var result = await sut.FindByIdOrEmailAsync(user.Id)!; + + Assert.Equal(user.Id, result!.Id); + Assert.Equal(user.Email, result!.Email); + + Assert.Equal(claims, result!.Claims); + } + + [Fact] + public async Task Should_query_many_by_email_async() + { + var (user1, claims1) = GernerateUser("id1"); + var (user2, claims2) = GernerateUser("id2"); + + var list = new List { user1, user2 }; + + A.CallTo(() => userManager.Users) + .Returns(list.AsQueryable()); + + A.CallTo(() => userManager.GetClaimsAsync(user2)) + .Returns(claims2); + + var result = await sut.QueryByEmailAsync("2"); + + Assert.Equal(user2.Id, result[0].Id); + Assert.Equal(user2.Email, result[0].Email); + + Assert.Equal(claims2, result[0].Claims); + + A.CallTo(() => userManager.GetClaimsAsync(user1)) + .MustNotHaveHappened(); + } + + private static (IdentityUser, List) GernerateUser(string id) + { + var user = new IdentityUser { Id = id, Email = $"email_{id}", NormalizedEmail = $"EMAIL_{id}" }; + + var claims = new List + { + new Claim($"{id}_a", "1"), + new Claim($"{id}_b", "2") + }; + + return (user, claims); + } + } +} diff --git a/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs similarity index 100% rename from tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs rename to backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj new file mode 100644 index 000000000..c7ad65860 --- /dev/null +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -0,0 +1,34 @@ + + + Exe + netcoreapp3.0 + Squidex.Domain.Users + 8.0 + enable + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/AssetExtensionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs new file mode 100644 index 000000000..2e3548b5a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs @@ -0,0 +1,164 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Assets +{ + public abstract class AssetStoreTests where T : IAssetStore + { + private readonly MemoryStream assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); + private readonly string fileName = Guid.NewGuid().ToString(); + private readonly string sourceFile = Guid.NewGuid().ToString(); + private readonly Lazy sut; + + protected T Sut + { + get { return sut.Value; } + } + + protected string FileName + { + get { return fileName; } + } + + protected AssetStoreTests() + { + sut = new Lazy(CreateStore); + } + + public abstract T CreateStore(); + + [Fact] + public virtual async Task Should_throw_exception_if_asset_to_download_is_not_found() + { + await Assert.ThrowsAsync(() => Sut.DownloadAsync(fileName, new MemoryStream())); + } + + [Fact] + public async Task Should_throw_exception_if_asset_to_copy_is_not_found() + { + await Assert.ThrowsAsync(() => Sut.CopyAsync(fileName, sourceFile)); + } + + [Fact] + public async Task Should_throw_exception_if_stream_to_download_is_null() + { + await Assert.ThrowsAsync(() => Sut.DownloadAsync("File", null!)); + } + + [Fact] + public async Task Should_throw_exception_if_stream_to_upload_is_null() + { + await Assert.ThrowsAsync(() => Sut.UploadAsync("File", null!)); + } + + [Fact] + public async Task Should_throw_exception_if_source_file_name_to_copy_is_empty() + { + await CheckEmpty(v => Sut.CopyAsync(v, "Target")); + } + + [Fact] + public async Task Should_throw_exception_if_target_file_name_to_copy_is_empty() + { + await CheckEmpty(v => Sut.CopyAsync("Source", v)); + } + + [Fact] + public async Task Should_throw_exception_if_file_name_to_delete_is_empty() + { + await CheckEmpty(v => Sut.DeleteAsync(v)); + } + + [Fact] + public async Task Should_throw_exception_if_file_name_to_download_is_empty() + { + await CheckEmpty(v => Sut.DownloadAsync(v, new MemoryStream())); + } + + [Fact] + public async Task Should_throw_exception_if_file_name_to_upload_is_empty() + { + await CheckEmpty(v => Sut.UploadAsync(v, new MemoryStream())); + } + + [Fact] + public async Task Should_write_and_read_file() + { + await Sut.UploadAsync(fileName, assetData); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(fileName, readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_write_and_read_file_and_overwrite_non_existing() + { + await Sut.UploadAsync(fileName, assetData, true); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(fileName, readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_write_and_read_overriding_file() + { + var oldData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }); + + await Sut.UploadAsync(fileName, oldData); + await Sut.UploadAsync(fileName, assetData, true); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(fileName, readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_throw_exception_when_file_to_write_already_exists() + { + await Sut.UploadAsync(fileName, assetData); + + await Assert.ThrowsAsync(() => Sut.UploadAsync(fileName, assetData)); + } + + [Fact] + public async Task Should_throw_exception_when_target_file_to_copy_to_already_exists() + { + await Sut.UploadAsync(sourceFile, assetData); + await Sut.CopyAsync(sourceFile, fileName); + + await Assert.ThrowsAsync(() => Sut.CopyAsync(sourceFile, fileName)); + } + + [Fact] + public async Task Should_ignore_when_deleting_not_existing_file() + { + await Sut.UploadAsync(sourceFile, assetData); + await Sut.DeleteAsync(sourceFile); + await Sut.DeleteAsync(sourceFile); + } + + private static async Task CheckEmpty(Func action) + { + await Assert.ThrowsAsync(() => action(null!)); + await Assert.ThrowsAsync(() => action(string.Empty)); + await Assert.ThrowsAsync(() => action(" ")); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FTPAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs new file mode 100644 index 000000000..12446e777 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Squidex.Infrastructure.Assets.ImageSharp; +using Xunit; + +namespace Squidex.Infrastructure.Assets +{ + public class ImageSharpAssetThumbnailGeneratorTests + { + private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); + private readonly MemoryStream target = new MemoryStream(); + + [Fact] + public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target); + + Assert.Equal(target.Length, source.Length); + } + + [Fact] + public async Task Should_resize_image_to_target() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize"); + + Assert.True(target.Length > source.Length); + } + + [Fact] + public async Task Should_change_jpeg_quality_and_write_to_target() + { + var source = GetJpeg(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + + [Fact] + public async Task Should_change_png_quality_and_write_to_target() + { + var source = GetPng(); + + await sut.CreateThumbnailAsync(source, target, quality: 10); + + Assert.True(target.Length < source.Length); + } + + [Fact] + public async Task Should_return_image_information_if_image_is_valid() + { + var source = GetPng(); + + var imageInfo = await sut.GetImageInfoAsync(source); + + Assert.Equal(600, imageInfo!.PixelHeight); + Assert.Equal(600, imageInfo!.PixelWidth); + } + + [Fact] + public async Task Should_return_null_if_stream_is_not_an_image() + { + var source = new MemoryStream(Convert.FromBase64String("YXNkc2Fk")); + + var imageInfo = await sut.GetImageInfoAsync(source); + + Assert.Null(imageInfo); + } + + private Stream GetPng() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png")!; + } + + private Stream GetJpeg() + { + return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg")!; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg rename to backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg diff --git a/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png b/backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png rename to backend/tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs new file mode 100644 index 000000000..223bb4ffb --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs @@ -0,0 +1,278 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class CollectionExtensionsTests + { + private readonly Dictionary valueDictionary = new Dictionary(); + private readonly Dictionary> listDictionary = new Dictionary>(); + + [Fact] + public void GetOrDefault_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrDefault(12)); + } + + [Fact] + public void GetOrDefault_should_return_default_and_not_add_it_if_key_not_exists() + { + Assert.Equal(0, valueDictionary.GetOrDefault(12)); + Assert.False(valueDictionary.ContainsKey(12)); + } + + [Fact] + public void GetOrAddDefault_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrAddDefault(12)); + } + + [Fact] + public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists() + { + Assert.Equal(0, valueDictionary.GetOrAddDefault(12)); + Assert.Equal(0, valueDictionary[12]); + } + + [Fact] + public void GetOrCreate_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34)); + } + + [Fact] + public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24)); + Assert.False(valueDictionary.ContainsKey(12)); + } + + [Fact] + public void GetOrAdd_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; + + Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 34)); + } + + [Fact] + public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrAdd(12, 24)); + Assert.Equal(24, valueDictionary[12]); + } + + [Fact] + public void GetOrAdd_should_return_default_and_add_it_with_fallback_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24)); + Assert.Equal(24, valueDictionary[12]); + } + + [Fact] + public void GetOrNew_should_return_value_if_key_exists() + { + var list = new List(); + listDictionary[12] = list; + + Assert.Equal(list, listDictionary.GetOrNew(12)); + } + + [Fact] + public void GetOrNew_should_return_default_but_not_add_it_if_key_not_exists() + { + var list = new List(); + + Assert.Equal(list, listDictionary.GetOrNew(12)); + Assert.False(listDictionary.ContainsKey(12)); + } + + [Fact] + public void GetOrAddNew_should_return_value_if_key_exists() + { + var list = new List(); + listDictionary[12] = list; + + Assert.Equal(list, listDictionary.GetOrAddNew(12)); + } + + [Fact] + public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists() + { + var list = new List(); + + Assert.Equal(list, listDictionary.GetOrAddNew(12)); + Assert.Equal(list, listDictionary[12]); + } + + [Fact] + public void SequentialHashCode_should_ignore_null_values() + { + var collection = new string?[] { null, null }; + + Assert.Equal(17, collection.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 5, 6 }; + + Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 4, 1 }; + + Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 6, 5, 3 }; + + Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void OrderedHashCode_should_return_same_hash_codes_for_list_with_same_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 5, 6 }; + + Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); + } + + [Fact] + public void OrderedHashCode_should_return_different_hash_codes_for_list_with_different_items() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 4, 1 }; + + Assert.NotEqual(collection2.OrderedHashCode(), collection1.OrderedHashCode()); + } + + [Fact] + public void OrderedHashCode_should_return_same_hash_codes_for_list_with_different_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 6, 5, 3 }; + + Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); + } + + [Fact] + public void EqualsDictionary_should_return_true_for_equal_dictionaries() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + + Assert.True(lhs.EqualsDictionary(rhs)); + } + + [Fact] + public void EqualsDictionary_should_return_false_for_different_sizes() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1 + }; + + Assert.False(lhs.EqualsDictionary(rhs)); + } + + [Fact] + public void EqualsDictionary_should_return_false_for_different_values() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [3] = 3 + }; + + Assert.False(lhs.EqualsDictionary(rhs)); + } + + [Fact] + public void Dictionary_should_return_same_hashcode_for_equal_dictionaries() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + + Assert.Equal(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); + } + + [Fact] + public void Dictionary_should_return_different_hashcode_for_different_dictionaries() + { + var lhs = new Dictionary + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary + { + [1] = 1, + [3] = 3 + }; + + Assert.NotEqual(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); + } + + [Fact] + public void Foreach_should_call_action_foreach_item() + { + var source = new List { 3, 5, 1 }; + var target = new List(); + + source.Foreach(target.Add); + + Assert.Equal(source, target); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs new file mode 100644 index 000000000..ccd843166 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Reflection; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class DomainObjectGrainFormatterTests + { + private readonly IGrainCallContext context = A.Fake(); + + [Fact] + public void Should_return_fallback_if_no_method_is_defined() + { + A.CallTo(() => context.InterfaceMethod) + .Returns(null!); + + var result = DomainObjectGrainFormatter.Format(context); + + Assert.Equal("Unknown", result); + } + + [Fact] + public void Should_return_method_name_if_not_domain_object_method() + { + var methodInfo = A.Fake(); + + A.CallTo(() => methodInfo.Name) + .Returns("Calculate"); + + A.CallTo(() => context.InterfaceMethod) + .Returns(methodInfo); + + var result = DomainObjectGrainFormatter.Format(context); + + Assert.Equal("Calculate", result); + } + + [Fact] + public void Should_return_nice_method_name_if_domain_object_execute() + { + var methodInfo = A.Fake(); + + A.CallTo(() => methodInfo.Name) + .Returns(nameof(IDomainObjectGrain.ExecuteAsync)); + + A.CallTo(() => context.Arguments) + .Returns(new object[] { new MyCommand() }); + + A.CallTo(() => context.InterfaceMethod) + .Returns(methodInfo); + + var result = DomainObjectGrainFormatter.Format(context); + + Assert.Equal("ExecuteAsync(MyCommand)", result); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs new file mode 100644 index 000000000..6803b7948 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -0,0 +1,220 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class DomainObjectGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyDomainObject sut; + + public sealed class MyDomainObject : DomainObjectGrain + { + public MyDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return Create(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturn(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return Update(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturn(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + } + + public DomainObjectGrainTests() + { + A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A.Ignored)) + .Returns(persistence); + + sut = new MyDomainObject(store); + } + + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_created() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntityCreatedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_updated() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(8, sut.Snapshot.Value); + Assert.Equal(1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_throw_exception_when_already_created() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + } + + [Fact] + public async Task Should_throw_exception_when_not_created() + { + await SetupEmptyAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + } + + [Fact] + public async Task Should_return_custom_result_on_create() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateCustom())); + + Assert.Equal("CREATED", result.Value); + } + + [Fact] + public async Task Should_return_custom_result_on_update() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateCustom())); + + Assert.Equal("UPDATED", result.Value); + } + + [Fact] + public async Task Should_throw_exception_when_other_verison_expected() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_create_failed() + { + await SetupEmptyAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(0, sut.Snapshot.Value); + Assert.Equal(-1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_update_failed() + { + await SetupCreatedAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + private async Task SetupCreatedAsync() + { + await sut.ActivateAsync(id); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + } + + private static J C(IAggregateCommand command) + { + return command.AsJ(); + } + + private async Task SetupEmptyAsync() + { + await sut.ActivateAsync(id); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs new file mode 100644 index 000000000..38d92a242 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs @@ -0,0 +1,280 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class LogSnapshotDomainObjectGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly IPersistence persistence = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyLogDomainObject sut; + + public sealed class MyLogDomainObject : LogSnapshotDomainObjectGrain + { + public MyLogDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return Create(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturn(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return Update(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturn(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + } + + public LogSnapshotDomainObjectGrainTests() + { + A.CallTo(() => store.WithEventSourcing(typeof(MyLogDomainObject), id, A.Ignored)) + .Returns(persistence); + + A.CallTo(() => store.GetSnapshotStore()) + .Returns(snapshotStore); + + sut = new MyLogDomainObject(store); + } + + [Fact] + public async Task Should_get_latestet_version_when_requesting_state_with_any() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Any); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_latestet_version_when_requesting_state_with_auto() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Auto); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_empty_version_when_requesting_state_with_empty_version() + { + await SetupUpdatedAsync(); + + var result = sut.GetSnapshot(EtagVersion.Empty); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); + } + + [Fact] + public async Task Should_get_specific_version_when_requesting_state_with_specific_version() + { + await SetupUpdatedAsync(); + + sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 4, Version = 0 }); + sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); + } + + [Fact] + public async Task Should_get_null_state_when_requesting_state_with_invalid_version() + { + await SetupUpdatedAsync(); + + Assert.Null(sut.GetSnapshot(-4)); + Assert.Null(sut.GetSnapshot(2)); + } + + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_created() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), -1, 0)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntityCreatedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_updated() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 8), 0, 1)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result.Value is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(8, sut.Snapshot.Value); + Assert.Equal(1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_throw_exception_when_already_created() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + } + + [Fact] + public async Task Should_throw_exception_when_not_created() + { + await SetupEmptyAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + } + + [Fact] + public async Task Should_return_custom_result_on_create() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(C(new CreateCustom())); + + Assert.Equal("CREATED", result.Value); + } + + [Fact] + public async Task Should_return_custom_result_on_update() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(C(new UpdateCustom())); + + Assert.Equal("UPDATED", result.Value); + } + + [Fact] + public async Task Should_throw_exception_when_other_verison_expected() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_create_failed() + { + await SetupEmptyAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, -1, 0)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(0, sut.Snapshot.Value); + Assert.Equal(-1, sut.Snapshot.Version); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_update_failed() + { + await SetupCreatedAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, 0, 1)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + + Assert.Empty(sut.GetUncomittedEvents()); + + Assert.Equal(4, sut.Snapshot.Value); + Assert.Equal(0, sut.Snapshot.Version); + } + + private async Task SetupCreatedAsync() + { + await sut.ActivateAsync(id); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + } + + private async Task SetupUpdatedAsync() + { + await SetupCreatedAsync(); + + await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + } + + private async Task SetupEmptyAsync() + { + await sut.ActivateAsync(id); + } + + private static J C(IAggregateCommand command) + { + return command.AsJ(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/CosmosDbEventStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs new file mode 100644 index 000000000..b76a66a62 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs @@ -0,0 +1,379 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing +{ + public abstract class EventStoreTests where T : IEventStore + { + private readonly Lazy sut; + private string subscriptionPosition; + + public sealed class EventSubscriber : IEventSubscriber + { + public List Events { get; } = new List(); + + public string LastPosition { get; set; } + + public Task OnErrorAsync(IEventSubscription subscription, Exception exception) + { + throw new NotSupportedException(); + } + + public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + LastPosition = storedEvent.EventPosition; + + Events.Add(storedEvent); + + return TaskHelper.Done; + } + } + + protected T Sut + { + get { return sut.Value; } + } + + protected abstract int SubscriptionDelayInMs { get; } + + protected EventStoreTests() + { + sut = new Lazy(CreateStore); + } + + public abstract T CreateStore(); + + [Fact] + public async Task Should_throw_exception_for_version_mismatch() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); + } + + [Fact] + public async Task Should_throw_exception_for_version_mismatch_and_update() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); + } + + [Fact] + public async Task Should_append_events() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + var readEvents1 = await QueryAsync(streamName); + var readEvents2 = await QueryWithCallbackAsync(streamName); + + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, events[0]), + new StoredEvent(streamName, "Position", 1, events[1]) + }; + + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } + + [Fact] + public async Task Should_subscribe_to_events() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + var readEvents = await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + }); + + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, events[0]), + new StoredEvent(streamName, "Position", 1, events[1]) + }; + + ShouldBeEquivalentTo(readEvents, expected); + } + + [Fact] + public async Task Should_subscribe_to_next_events() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events1 = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, events1); + }); + + var events2 = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + var readEventsFromPosition = await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, events2); + }); + + var expectedFromPosition = new[] + { + new StoredEvent(streamName, "Position", 2, events2[0]), + new StoredEvent(streamName, "Position", 3, events2[1]) + }; + + var readEventsFromBeginning = await QueryWithSubscriptionAsync(streamName, fromBeginning: true); + + var expectedFromBeginning = new[] + { + new StoredEvent(streamName, "Position", 0, events1[0]), + new StoredEvent(streamName, "Position", 1, events1[1]), + new StoredEvent(streamName, "Position", 2, events2[0]), + new StoredEvent(streamName, "Position", 3, events2[1]) + }; + + ShouldBeEquivalentTo(readEventsFromPosition, expectedFromPosition); + + ShouldBeEquivalentTo(readEventsFromBeginning, expectedFromBeginning); + } + + [Fact] + public async Task Should_read_events_from_offset() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + var firstRead = await QueryAsync(streamName); + + var readEvents1 = await QueryAsync(streamName, 1); + var readEvents2 = await QueryWithCallbackAsync(streamName, firstRead[0].EventPosition); + + var expected = new[] + { + new StoredEvent(streamName, "Position", 1, events[1]) + }; + + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } + + [Fact] + public async Task Should_delete_stream() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + new EventData("Type1", new EnvelopeHeaders(), "1"), + new EventData("Type2", new EnvelopeHeaders(), "2") + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + await Sut.DeleteStreamAsync(streamName); + + var readEvents = await QueryAsync(streamName); + + Assert.Empty(readEvents); + } + + [Fact] + public async Task Should_query_events_by_property() + { + var keyed1 = new EnvelopeHeaders(); + var keyed2 = new EnvelopeHeaders(); + + keyed1.Add("key", Guid.NewGuid().ToString()); + keyed2.Add("key", Guid.NewGuid().ToString()); + + var streamName1 = $"test-{Guid.NewGuid()}"; + var streamName2 = $"test-{Guid.NewGuid()}"; + + var events1 = new[] + { + new EventData("Type1", keyed1, "1"), + new EventData("Type2", keyed2, "2") + }; + + var events2 = new[] + { + new EventData("Type3", keyed2, "3"), + new EventData("Type4", keyed1, "4") + }; + + await Sut.CreateIndexAsync("key"); + + await Sut.AppendAsync(Guid.NewGuid(), streamName1, events1); + await Sut.AppendAsync(Guid.NewGuid(), streamName2, events2); + + var readEvents = await QueryWithFilterAsync("key", keyed2["key"].ToString()); + + var expected = new[] + { + new StoredEvent(streamName1, "Position", 1, events1[1]), + new StoredEvent(streamName2, "Position", 0, events2[0]) + }; + + ShouldBeEquivalentTo(readEvents, expected); + } + + private Task> QueryAsync(string streamName, long position = EtagVersion.Any) + { + return Sut.QueryAsync(streamName, position); + } + + private async Task?> QueryWithFilterAsync(string property, object value) + { + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) + { + var readEvents = new List(); + + await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, property, value, null, cts.Token); + + await Task.Delay(500, cts.Token); + + if (readEvents.Count > 0) + { + return readEvents; + } + } + + cts.Token.ThrowIfCancellationRequested(); + + return null; + } + } + + private async Task?> QueryWithCallbackAsync(string? streamFilter = null, string? position = null) + { + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) + { + var readEvents = new List(); + + await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, streamFilter, position, cts.Token); + + await Task.Delay(500, cts.Token); + + if (readEvents.Count > 0) + { + return readEvents; + } + } + + cts.Token.ThrowIfCancellationRequested(); + + return null; + } + } + + private async Task?> QueryWithSubscriptionAsync(string streamFilter, Func? action = null, bool fromBeginning = false) + { + var subscriber = new EventSubscriber(); + + IEventSubscription? subscription = null; + try + { + subscription = Sut.CreateSubscription(subscriber, streamFilter, fromBeginning ? null : subscriptionPosition); + + if (action != null) + { + await action(); + } + + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) + { + subscription.WakeUp(); + + await Task.Delay(500, cts.Token); + + if (subscriber.Events.Count > 0) + { + subscriptionPosition = subscriber.LastPosition; + + return subscriber.Events; + } + } + + cts.Token.ThrowIfCancellationRequested(); + + return null; + } + } + finally + { + if (subscription != null) + { + await subscription.StopAsync(); + } + } + } + + private static void ShouldBeEquivalentTo(IEnumerable? actual, params StoredEvent[] expected) + { + var actualArray = actual.Select(x => new StoredEvent(x.StreamName, "Position", x.EventStreamNumber, x.Data)).ToArray(); + + actualArray.Should().BeEquivalentTo(expected); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs new file mode 100644 index 000000000..27ecdb583 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -0,0 +1,409 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Orleans.Concurrency; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerGrainTests + { + public sealed class MyEventConsumerGrain : EventConsumerGrain + { + public MyEventConsumerGrain( + EventConsumerFactory eventConsumerFactory, + IGrainState state, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISemanticLog log) + : base(eventConsumerFactory, state, eventStore, eventDataFormatter, log) + { + } + + protected override IEventConsumerGrain GetSelf() + { + return this; + } + + protected override IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string? streamFilter, string? position) + { + return store.CreateSubscription(subscriber, streamFilter, position); + } + } + + private readonly IGrainState grainState = A.Fake>(); + private readonly IEventConsumer eventConsumer = A.Fake(); + private readonly IEventStore eventStore = A.Fake(); + private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly IEventDataFormatter formatter = A.Fake(); + private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); + private readonly Envelope envelope = new Envelope(new MyEvent()); + private readonly EventConsumerGrain sut; + private readonly string consumerName; + private readonly string initialPosition = Guid.NewGuid().ToString(); + + public EventConsumerGrainTests() + { + grainState.Value.Position = initialPosition; + + consumerName = eventConsumer.GetType().Name; + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .Returns(eventSubscription); + + A.CallTo(() => eventConsumer.Name) + .Returns(consumerName); + + A.CallTo(() => eventConsumer.Handles(A.Ignored)) + .Returns(true); + + A.CallTo(() => formatter.Parse(eventData, null)) + .Returns(envelope); + + sut = new MyEventConsumerGrain( + x => eventConsumer, + grainState, + eventStore, + formatter, + log); + } + + [Fact] + public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() + { + grainState.Value = grainState.Value.Stopped(); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_subscribe_to_event_store_when_not_found_in_db() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_stop_subscription_when_stopped() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.StopAsync(); + await sut.StopAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_reset_consumer_when_resetting() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.StopAsync(); + await sut.ResetAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventConsumer.ClearAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, grainState.Value.Position)) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, null)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_invoke_and_update_position_when_event_received() + { + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_not_invoke_but_update_position_when_consumer_does_not_want_to_handle() + { + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + A.CallTo(() => eventConsumer.Handles(@event)) + .Returns(false); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_ignore_old_events() + { + A.CallTo(() => formatter.Parse(eventData, null)) + .Throws(new TypeNameNotFoundException()); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() + { + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(A.Fake(), @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_stop_if_consumer_failed() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + var ex = new InvalidOperationException(); + + await OnErrorAsync(eventSubscription, ex); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() + { + var ex = new InvalidOperationException(); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnErrorAsync(A.Fake(), ex); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_wakeup_when_already_subscribed() + { + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.ActivateAsync(); + + A.CallTo(() => eventSubscription.WakeUp()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_stop_if_resetting_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => eventConsumer.ClearAsync()) + .Throws(ex); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.ResetAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_stop_if_handling_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => eventConsumer.On(envelope)) + .Throws(ex); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_stop_if_deserialization_failed() + { + var ex = new InvalidOperationException(); + + A.CallTo(() => formatter.Parse(eventData, null)) + .Throws(ex); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + [Fact] + public async Task Should_start_after_stop_when_handling_failed() + { + var exception = new InvalidOperationException(); + + A.CallTo(() => eventConsumer.On(envelope)) + .Throws(exception); + + var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); + + await sut.ActivateAsync(consumerName); + await sut.ActivateAsync(); + + await OnEventAsync(eventSubscription, @event); + + await sut.StopAsync(); + await sut.StartAsync(); + await sut.StartAsync(); + + grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(1, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(2, Times.Exactly); + } + + private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) + { + return sut.OnErrorAsync(subscriber.AsImmutable(), ex.AsImmutable()); + } + + private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) + { + return sut.OnEventAsync(subscriber.AsImmutable(), ev.AsImmutable()); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs new file mode 100644 index 000000000..2d29eb56a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs @@ -0,0 +1,186 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Orleans; +using Orleans.Concurrency; +using Orleans.Core; +using Orleans.Runtime; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerManagerGrainTests + { + public class MyEventConsumerManagerGrain : EventConsumerManagerGrain + { + public MyEventConsumerManagerGrain( + IEnumerable eventConsumers, + IGrainIdentity identity, + IGrainRuntime runtime) + : base(eventConsumers, identity, runtime) + { + } + } + + private readonly IEventConsumer consumerA = A.Fake(); + private readonly IEventConsumer consumerB = A.Fake(); + private readonly IEventConsumerGrain grainA = A.Fake(); + private readonly IEventConsumerGrain grainB = A.Fake(); + private readonly MyEventConsumerManagerGrain sut; + + public EventConsumerManagerGrainTests() + { + var grainRuntime = A.Fake(); + var grainFactory = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain("a", null)).Returns(grainA); + A.CallTo(() => grainFactory.GetGrain("b", null)).Returns(grainB); + A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); + + A.CallTo(() => consumerA.Name).Returns("a"); + A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); + + A.CallTo(() => consumerB.Name).Returns("b"); + A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); + + sut = new MyEventConsumerManagerGrain(new[] { consumerA, consumerB }, A.Fake(), grainRuntime); + } + + [Fact] + public async Task Should_not_activate_all_grains_on_activate() + { + await sut.OnActivateAsync(); + + A.CallTo(() => grainA.ActivateAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_activate_all_grains_on_reminder() + { + await sut.ReceiveReminder(null!, default); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_activate_all_grains_on_wakeup_with_null() + { + await sut.ActivateAsync(null); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_activate_matching_grains_when_stream_name_defined() + { + await sut.ActivateAsync("a-123"); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_start_all_grains() + { + await sut.StartAllAsync(); + + A.CallTo(() => grainA.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StartAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_start_matching_grain() + { + await sut.StartAsync("a"); + + A.CallTo(() => grainA.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StartAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_stop_all_grains() + { + await sut.StopAllAsync(); + + A.CallTo(() => grainA.StopAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StopAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_stop_matching_grain() + { + await sut.StopAsync("b"); + + A.CallTo(() => grainA.StopAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.StopAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_reset_matching_grain() + { + await sut.ResetAsync("b"); + + A.CallTo(() => grainA.ResetAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.ResetAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_fetch_infos_from_all_grains() + { + A.CallTo(() => grainA.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); + + A.CallTo(() => grainB.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); + + var infos = await sut.GetConsumersAsync(); + + infos.Value.Should().BeEquivalentTo( + new List + { + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } + }); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs new file mode 100644 index 000000000..5564a2147 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing +{ + public class RetrySubscriptionTests + { + private readonly IEventStore eventStore = A.Fake(); + private readonly IEventSubscriber eventSubscriber = A.Fake(); + private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly IEventSubscriber sutSubscriber; + private readonly RetrySubscription sut; + private readonly string streamFilter = Guid.NewGuid().ToString(); + + public RetrySubscriptionTests() + { + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)).Returns(eventSubscription); + + sut = new RetrySubscription(eventStore, eventSubscriber, streamFilter, null) { ReconnectWaitMs = 50 }; + + sutSubscriber = sut; + } + + [Fact] + public async Task Should_subscribe_after_constructor() + { + await sut.StopAsync(); + + A.CallTo(() => eventStore.CreateSubscription(sut, streamFilter, null)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_reopen_subscription_once_when_exception_is_retrieved() + { + await OnErrorAsync(eventSubscription, new InvalidOperationException()); + + await Task.Delay(1000); + + await sut.StopAsync(); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(2, Times.Exactly); + + A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_forward_error_from_inner_subscription_when_failed_often() + { + var ex = new InvalidOperationException(); + + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await OnErrorAsync(null!, ex); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_forward_error_when_exception_is_from_another_subscription() + { + var ex = new InvalidOperationException(); + + await OnErrorAsync(A.Fake(), ex); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_forward_event_from_inner_subscription() + { + var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + + await OnEventAsync(eventSubscription, ev); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnEventAsync(sut, ev)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_forward_event_when_message_is_from_another_subscription() + { + var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + + await OnEventAsync(A.Fake(), ev); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnEventAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) + { + return sutSubscriber.OnErrorAsync(subscriber, ex); + } + + private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) + { + return sutSubscriber.OnEventAsync(subscriber, ev); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs new file mode 100644 index 000000000..8761ccd94 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs @@ -0,0 +1,367 @@ +// ========================================================================== +// 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.Validation; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class GuardTests + { + private sealed class MyValidatableValid : IValidatable + { + public void Validate(IList errors) + { + } + } + + private sealed class MyValidatableInvalid : IValidatable + { + public void Validate(IList errors) + { + errors.Add(new ValidationError("error.", "error")); + } + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void NotNullOrEmpty_should_throw_for_empy_strings(string invalidString) + { + Assert.Throws(() => Guard.NotNullOrEmpty(invalidString, "parameter")); + } + + [Fact] + public void NotNullOrEmpty_should_throw_for_null_string() + { + Assert.Throws(() => Guard.NotNullOrEmpty(null, "parameter")); + } + + [Fact] + public void NotNullOrEmpty_should_do_nothing_for_vaid_string() + { + Guard.NotNullOrEmpty("value", "parameter"); + } + + [Fact] + public void NotNull_should_throw_for_null_value() + { + Assert.Throws(() => Guard.NotNull(null, "parameter")); + } + + [Fact] + public void NotNull_should_do_nothing_for_valid_value() + { + Guard.NotNull("value", "parameter"); + } + + [Fact] + public void Enum_should_throw_for_invalid_enum() + { + Assert.Throws(() => Guard.Enum((DateTimeKind)13, "Parameter")); + } + + [Fact] + public void Enum_should_do_nothing_for_valid_enum() + { + Guard.Enum(DateTimeKind.Local, "Parameter"); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_guid() + { + Assert.Throws(() => Guard.NotEmpty(Guid.Empty, "parameter")); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_valid_guid() + { + Guard.NotEmpty(Guid.NewGuid(), "parameter"); + } + + [Fact] + public void HasType_should_throw_for_other_type() + { + Assert.Throws(() => Guard.HasType("value", "parameter")); + } + + [Fact] + public void HasType_should_do_nothing_for_null_value() + { + Guard.HasType(null, "parameter"); + } + + [Fact] + public void HasType_should_do_nothing_for_correct_type() + { + Guard.HasType(123, "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_throw_for_other_type() + { + Assert.Throws(() => Guard.HasType("value", typeof(int), "parameter")); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_null_value() + { + Guard.HasType(null, typeof(int), "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_correct_type() + { + Guard.HasType(123, typeof(int), "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_null_type() + { + Guard.HasType(123, null, "parameter"); + } + + [Fact] + public void NotDefault_should_throw_for_default_values() + { + Assert.Throws(() => Guard.NotDefault(Guid.Empty, "parameter")); + Assert.Throws(() => Guard.NotDefault(0, "parameter")); + Assert.Throws(() => Guard.NotDefault((string?)null, "parameter")); + Assert.Throws(() => Guard.NotDefault(false, "parameter")); + } + + [Fact] + public void NotDefault_should_do_nothing_for_non_default_value() + { + Guard.NotDefault(Guid.NewGuid(), "parameter"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Slug ")] + [InlineData(" not--a--slug ")] + [InlineData(" not-a-slug ")] + [InlineData("-not-a-slug-")] + [InlineData("not$-a-slug")] + [InlineData("not-a-Slug")] + public void ValidSlug_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws(() => Guard.ValidSlug(slug, "parameter")); + } + + [Theory] + [InlineData("slug")] + [InlineData("slug23")] + [InlineData("other-slug")] + [InlineData("just-another-slug")] + public void ValidSlug_should_do_nothing_for_valid_slugs(string slug) + { + Guard.ValidSlug(slug, "parameter"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Property ")] + [InlineData(" not--a--property ")] + [InlineData(" not-a-property ")] + [InlineData("-not-a-property-")] + [InlineData("not$-a-property")] + public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws(() => Guard.ValidPropertyName(slug, "property")); + } + + [Theory] + [InlineData("property")] + [InlineData("property23")] + [InlineData("other-property")] + [InlineData("other-Property")] + [InlineData("otherProperty")] + [InlineData("just-another-property")] + [InlineData("just-Another-Property")] + [InlineData("justAnotherProperty")] + public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) + { + Guard.ValidPropertyName(property, "parameter"); + } + + [Theory] + [InlineData(double.PositiveInfinity)] + [InlineData(double.NegativeInfinity)] + [InlineData(double.NaN)] + public void ValidNumber_should_throw_for_invalid_doubles(double value) + { + Assert.Throws(() => Guard.ValidNumber(value, "parameter")); + } + + [Theory] + [InlineData(0d)] + [InlineData(-1000d)] + [InlineData(1000d)] + public void ValidNumber_do_nothing_for_valid_double(double value) + { + Guard.ValidNumber(value, "parameter"); + } + + [Theory] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(float.NaN)] + public void ValidNumber_should_throw_for_invalid_float(float value) + { + Assert.Throws(() => Guard.ValidNumber(value, "parameter")); + } + + [Theory] + [InlineData(0f)] + [InlineData(-1000f)] + [InlineData(1000f)] + public void ValidNumber_do_nothing_for_valid_float(float value) + { + Guard.ValidNumber(value, "parameter"); + } + + [Theory] + [InlineData(4)] + [InlineData(104)] + public void Between_should_throw_for_values_outside_of_range(int value) + { + Assert.Throws(() => Guard.Between(value, 10, 100, "parameter")); + } + + [Theory] + [InlineData(10)] + [InlineData(55)] + [InlineData(100)] + public void Between_should_do_nothing_for_values_in_range(int value) + { + Guard.Between(value, 10, 100, "parameter"); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public void GreaterThan_should_throw_for_smaller_values(int value) + { + Assert.Throws(() => Guard.GreaterThan(value, 100, "parameter")); + } + + [Theory] + [InlineData(101)] + [InlineData(200)] + public void GreaterThan_should_do_nothing_for_greater_values(int value) + { + Guard.GreaterThan(value, 100, "parameter"); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + public void GreaterEquals_should_throw_for_smaller_values(int value) + { + Assert.Throws(() => Guard.GreaterEquals(value, 100, "parameter")); + } + + [Theory] + [InlineData(100)] + [InlineData(200)] + public void GreaterEquals_should_do_nothing_for_greater_values(int value) + { + Guard.GreaterEquals(value, 100, "parameter"); + } + + [Theory] + [InlineData(1000)] + [InlineData(100)] + public void LessThan_should_throw_for_greater_values(int value) + { + Assert.Throws(() => Guard.LessThan(value, 100, "parameter")); + } + + [Theory] + [InlineData(99)] + [InlineData(50)] + public void LessThan_should_do_nothing_for_smaller_values(int value) + { + Guard.LessThan(value, 100, "parameter"); + } + + [Theory] + [InlineData(1000)] + [InlineData(101)] + public void LessEquals_should_throw_for_greater_values(int value) + { + Assert.Throws(() => Guard.LessEquals(value, 100, "parameter")); + } + + [Theory] + [InlineData(100)] + [InlineData(50)] + public void LessEquals_should_do_nothing_for_smaller_values(int value) + { + Guard.LessEquals(value, 100, "parameter"); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_collection() + { + Assert.Throws(() => Guard.NotEmpty(new int[0], "parameter")); + } + + [Fact] + public void NotEmpty_should_throw_for_null_collection() + { + Assert.Throws(() => Guard.NotEmpty((int[]?)null, "parameter")); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_value_collection() + { + Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); + } + + [Fact] + public void ValidFileName_should_throw_for_invalid_file_name() + { + Assert.Throws(() => Guard.ValidFileName("File/Name", "Parameter")); + } + + [Fact] + public void ValidFileName_should_throw_for_null_file_name() + { + Assert.Throws(() => Guard.ValidFileName(null, "Parameter")); + } + + [Fact] + public void ValidFileName_should_do_nothing_for_valid_file_name() + { + Guard.ValidFileName("FileName", "Parameter"); + } + + [Fact] + public void Valid_should_throw_exception_if_null() + { + Assert.Throws(() => Guard.Valid(null, "Parameter", () => "Message")); + } + + [Fact] + public void Valid_should_throw_exception_if_invalid() + { + Assert.Throws(() => Guard.Valid(new MyValidatableInvalid(), "Parameter", () => "Message")); + } + + [Fact] + public void Valid_should_do_nothing_if_valid() + { + Guard.Valid(new MyValidatableValid(), "Parameter", () => "Message"); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs new file mode 100644 index 000000000..060b5e4cf --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Xunit; + +#pragma warning disable SA1122 // Use string.Empty for empty strings + +namespace Squidex.Infrastructure.Http +{ + public class DumpFormatterTests + { + [Fact] + public void Should_format_dump_without_response() + { + var httpRequest = CreateRequest(); + + var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "", + "", + "Response:", + "Timeout after 00:01:00"); + + Assert.Equal(expected, dump); + } + + [Fact] + public void Should_format_dump_without_content() + { + var httpRequest = CreateRequest(); + var httpResponse = CreateResponse(); + + var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "", + "", + "Response:", + "HTTP/1.1 200 OK", + "Transfer-Encoding: UTF-8", + "Trailer: Expires", + "", + "Elapsed: 00:01:00"); + + Assert.Equal(expected, dump); + } + + [Fact] + public void Should_format_dump_with_content_without_timeout() + { + var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); + var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); + + var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "Content-Type: text/plain; charset=utf-8", + "", + "Hello Squidex", + "", + "", + "Response:", + "HTTP/1.1 200 OK", + "Transfer-Encoding: UTF-8", + "Trailer: Expires", + "Content-Type: text/plain; charset=utf-8", + "", + "Hello Back", + "", + "Elapsed: 00:01:00"); + + Assert.Equal(expected, dump); + } + + private static HttpRequestMessage CreateRequest(HttpContent? content = null) + { + var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); + + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); + + request.Content = content; + + return request; + } + + private static HttpResponseMessage CreateResponse(HttpContent? content = null) + { + var response = new HttpResponseMessage(HttpStatusCode.OK); + + response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); + response.Headers.Trailer.Add("Expires"); + + response.Content = content; + + return response; + } + + private static string CreateExpectedDump(params string[] input) + { + return string.Join(Environment.NewLine, input) + Environment.NewLine; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs b/backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/InstantExtensions.cs rename to backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs new file mode 100644 index 000000000..90886125b --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public class ClaimsPrincipalConverterTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var value = new ClaimsPrincipal( + new[] + { + new ClaimsIdentity( + new[] + { + new Claim("email", "me@email.com"), + new Claim("username", "me@email.com") + }, + "Cookie"), + new ClaimsIdentity( + new[] + { + new Claim("user_id", "12345"), + new Claim("login", "me") + }, + "Google") + }); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, serialized.Identities.ElementAt(0).AuthenticationType); + Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, serialized.Identities.ElementAt(1).AuthenticationType); + } + + [Fact] + public void Should_serialize_and_deserialize_null_principal() + { + ClaimsPrincipal? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Null(serialized); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs new file mode 100644 index 000000000..e6706d506 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs @@ -0,0 +1,357 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NodaTime; +using Xunit; + +namespace Squidex.Infrastructure.Json.Objects +{ + public class JsonObjectTests + { + [Fact] + public void Should_make_correct_object_equal_comparisons() + { + var obj_count1_key1_val1_a = JsonValue.Object().Add("key1", 1); + var obj_count1_key1_val1_b = JsonValue.Object().Add("key1", 1); + + var obj_count1_key1_val2 = JsonValue.Object().Add("key1", 2); + var obj_count1_key2_val1 = JsonValue.Object().Add("key2", 1); + var obj_count2_key1_val1 = JsonValue.Object().Add("key1", 1).Add("key2", 2); + + var number = JsonValue.Create(1); + + Assert.Equal(obj_count1_key1_val1_a, obj_count1_key1_val1_b); + Assert.Equal(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val1_b.GetHashCode()); + Assert.True(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val1_b)); + + Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key1_val2); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val2.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val2)); + + Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key2_val1); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key2_val1.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key2_val1)); + + Assert.NotEqual(obj_count1_key1_val1_a, obj_count2_key1_val1); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count2_key1_val1.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count2_key1_val1)); + + Assert.NotEqual(obj_count1_key1_val1_a, number); + Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), number.GetHashCode()); + Assert.False(obj_count1_key1_val1_a.Equals((object)number)); + } + + [Fact] + public void Should_make_correct_array_equal_comparisons() + { + var array_count1_val1_a = JsonValue.Array(1); + var array_count1_val1_b = JsonValue.Array(1); + + var array_count1_val2 = JsonValue.Array(2); + var array_count2_val1 = JsonValue.Array(1, 2); + + var number = JsonValue.Create(1); + + Assert.Equal(array_count1_val1_a, array_count1_val1_b); + Assert.Equal(array_count1_val1_a.GetHashCode(), array_count1_val1_b.GetHashCode()); + Assert.True(array_count1_val1_a.Equals((object)array_count1_val1_b)); + + Assert.NotEqual(array_count1_val1_a, array_count1_val2); + Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count1_val2.GetHashCode()); + Assert.False(array_count1_val1_a.Equals((object)array_count1_val2)); + + Assert.NotEqual(array_count1_val1_a, array_count2_val1); + Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count2_val1.GetHashCode()); + Assert.False(array_count1_val1_a.Equals((object)array_count2_val1)); + + Assert.NotEqual(array_count1_val1_a, number); + Assert.NotEqual(array_count1_val1_a.GetHashCode(), number.GetHashCode()); + Assert.False(array_count1_val1_a.Equals((object)number)); + } + + [Fact] + public void Should_make_correct_array_scalar_comparisons() + { + var number_val1_a = JsonValue.Create(1); + var number_val1_b = JsonValue.Create(1); + + var number_val2 = JsonValue.Create(2); + + var boolean = JsonValue.True; + + Assert.Equal(number_val1_a, number_val1_b); + Assert.Equal(number_val1_a.GetHashCode(), number_val1_b.GetHashCode()); + Assert.True(number_val1_a.Equals((object)number_val1_b)); + + Assert.NotEqual(number_val1_a, number_val2); + Assert.NotEqual(number_val1_a.GetHashCode(), number_val2.GetHashCode()); + Assert.False(number_val1_a.Equals((object)number_val2)); + + Assert.NotEqual(number_val1_a, boolean); + Assert.NotEqual(number_val1_a.GetHashCode(), boolean.GetHashCode()); + Assert.False(number_val1_a.Equals((object)boolean)); + } + + [Fact] + public void Should_make_correct_null_comparisons() + { + var null_a = JsonValue.Null; + var null_b = JsonValue.Null; + + var boolean = JsonValue.True; + + Assert.Equal(null_a, null_b); + Assert.Equal(null_a.GetHashCode(), null_b.GetHashCode()); + Assert.True(null_a.Equals((object)null_b)); + + Assert.NotEqual(null_a, boolean); + Assert.NotEqual(null_a.GetHashCode(), boolean.GetHashCode()); + Assert.False(null_a.Equals((object)boolean)); + } + + [Fact] + public void Should_cache_null() + { + Assert.Same(JsonValue.Null, JsonValue.Create((string?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((bool?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((double?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((object?)null)); + Assert.Same(JsonValue.Null, JsonValue.Create((Instant?)null)); + } + + [Fact] + public void Should_cache_true() + { + Assert.Same(JsonValue.True, JsonValue.Create(true)); + } + + [Fact] + public void Should_cache_false() + { + Assert.Same(JsonValue.False, JsonValue.Create(false)); + } + + [Fact] + public void Should_cache_empty() + { + Assert.Same(JsonValue.Empty, JsonValue.Create(string.Empty)); + } + + [Fact] + public void Should_cache_zero() + { + Assert.Same(JsonValue.Zero, JsonValue.Create(0)); + } + + [Fact] + public void Should_boolean_from_object() + { + Assert.Equal(JsonValue.True, JsonValue.Create((object)true)); + } + + [Fact] + public void Should_create_value_from_instant() + { + var instant = Instant.FromUnixTimeSeconds(4123125455); + + Assert.Equal(instant.ToString(), JsonValue.Create(instant).ToString()); + } + + [Fact] + public void Should_create_value_from_instant_object() + { + var instant = Instant.FromUnixTimeSeconds(4123125455); + + Assert.Equal(instant.ToString(), JsonValue.Create((object)instant).ToString()); + } + + [Fact] + public void Should_create_array() + { + var json = JsonValue.Array(1, "2"); + + Assert.Equal("[1, \"2\"]", json.ToJsonString()); + Assert.Equal("[1, \"2\"]", json.ToString()); + } + + [Fact] + public void Should_create_object() + { + var json = JsonValue.Object().Add("key1", 1).Add("key2", "2"); + + Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToJsonString()); + Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToString()); + } + + [Fact] + public void Should_create_number() + { + var json = JsonValue.Create(123); + + Assert.Equal("123", json.ToJsonString()); + Assert.Equal("123", json.ToString()); + } + + [Fact] + public void Should_create_boolean_true() + { + var json = JsonValue.Create(true); + + Assert.Equal("true", json.ToJsonString()); + Assert.Equal("true", json.ToString()); + } + + [Fact] + public void Should_create_boolean_false() + { + var json = JsonValue.Create(false); + + Assert.Equal("false", json.ToJsonString()); + Assert.Equal("false", json.ToString()); + } + + [Fact] + public void Should_create_string() + { + var json = JsonValue.Create("hi"); + + Assert.Equal("\"hi\"", json.ToJsonString()); + Assert.Equal("hi", json.ToString()); + } + + [Fact] + public void Should_create_null() + { + var json = JsonValue.Create((object?)null); + + Assert.Equal("null", json.ToJsonString()); + Assert.Equal("null", json.ToString()); + } + + [Fact] + public void Should_create_arrays_in_different_ways() + { + var numbers = new[] + { + JsonValue.Array(1.0f, 2.0f), + JsonValue.Array(JsonValue.Create(1.0f), JsonValue.Create(2.0f)) + }; + + Assert.Single(numbers.Distinct()); + Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); + } + + [Fact] + public void Should_create_number_from_types() + { + var numbers = new[] + { + JsonValue.Create(12.0f), + JsonValue.Create(12.0), + JsonValue.Create(12L), + JsonValue.Create(12), + JsonValue.Create((object)12.0d), + JsonValue.Create((double?)12.0d) + }; + + Assert.Single(numbers.Distinct()); + Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); + } + + [Fact] + public void Should_create_null_when_adding_null_to_array() + { + var array = JsonValue.Array(); + + array.Add(null!); + + Assert.Same(JsonValue.Null, array[0]); + } + + [Fact] + public void Should_create_null_when_replacing_to_null_in_array() + { + var array = JsonValue.Array(1); + + array[0] = null!; + + Assert.Same(JsonValue.Null, array[0]); + } + + [Fact] + public void Should_create_null_when_adding_null_to_object() + { + var obj = JsonValue.Object(); + + obj.Add("key", null!); + + Assert.Same(JsonValue.Null, obj["key"]); + } + + [Fact] + public void Should_create_null_when_replacing_to_null_object() + { + var obj = JsonValue.Object(); + + obj["key"] = null!; + + Assert.Same(JsonValue.Null, obj["key"]); + } + + [Fact] + public void Should_remove_value_from_object() + { + var obj = JsonValue.Object().Add("key", 1); + + obj.Remove("key"); + + Assert.False(obj.TryGetValue("key", out _)); + Assert.False(obj.ContainsKey("key")); + } + + [Fact] + public void Should_clear_values_from_object() + { + var obj = JsonValue.Object().Add("key", 1); + + obj.Clear(); + + Assert.False(obj.TryGetValue("key", out _)); + Assert.False(obj.ContainsKey("key")); + } + + [Fact] + public void Should_provide_collection_values_from_object() + { + var obj = JsonValue.Object().Add("11", "44").Add("22", "88"); + + var kvps = new[] + { + new KeyValuePair("11", JsonValue.Create("44")), + new KeyValuePair("22", JsonValue.Create("88")) + }; + + Assert.Equal(2, obj.Count); + + Assert.Equal(new[] { "11", "22" }, obj.Keys); + Assert.Equal(new[] { "44", "88" }, obj.Values.Select(x => x.ToString())); + + Assert.Equal(kvps, obj.ToArray()); + Assert.Equal(kvps, ((IEnumerable)obj).OfType>().ToArray()); + } + + [Fact] + public void Should_throw_exception_when_creation_value_from_invalid_type() + { + Assert.Throws(() => JsonValue.Create(Guid.Empty)); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs new file mode 100644 index 000000000..c49223413 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -0,0 +1,141 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class LanguageTests + { + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Should_throw_exception_if_getting_by_empty_key(string key) + { + Assert.Throws(() => Language.GetLanguage(key)); + } + + [Fact] + public void Should_throw_exception_if_getting_by_null_key() + { + Assert.Throws(() => Language.GetLanguage(null!)); + } + + [Fact] + public void Should_throw_exception_if_getting_by_unsupported_language() + { + Assert.Throws(() => Language.GetLanguage("xy")); + } + + [Fact] + public void Should_provide_all_languages() + { + Assert.True(Language.AllLanguages.Count > 100); + } + + [Fact] + public void Should_return_true_for_valid_language() + { + Assert.True(Language.IsValidLanguage("de")); + } + + [Fact] + public void Should_return_false_for_invalid_language() + { + Assert.False(Language.IsValidLanguage("xx")); + } + + [Fact] + public void Should_make_implicit_conversion_to_language() + { + Language language = "de"!; + + Assert.Equal(Language.DE, language); + } + + [Fact] + public void Should_make_implicit_conversion_to_string() + { + string iso2Code = Language.DE!; + + Assert.Equal("de", iso2Code); + } + + [Theory] + [InlineData("de", "German")] + [InlineData("en", "English")] + [InlineData("sv", "Swedish")] + [InlineData("zh", "Chinese")] + public void Should_provide_correct_english_name(string key, string englishName) + { + var language = Language.GetLanguage(key); + + Assert.Equal(key, language.Iso2Code); + Assert.Equal(englishName, language.EnglishName); + Assert.Equal(englishName, language.ToString()); + } + + [Theory] + [InlineData("en", "en")] + [InlineData("en ", "en")] + [InlineData("EN", "en")] + [InlineData("EN ", "en")] + public void Should_parse_valid_languages(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + + [Theory] + [InlineData("en-US", "en")] + [InlineData("en-GB", "en")] + [InlineData("EN-US", "en")] + [InlineData("EN-GB", "en")] + public void Should_parse_lanuages_from_culture(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("xx")] + [InlineData("invalid")] + [InlineData(null)] + public void Should_parse_invalid_languages(string input) + { + var language = Language.ParseOrNull(input); + + Assert.Null(language); + } + + [Fact] + public void Should_serialize_and_deserialize_null_language() + { + Language? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_language() + { + var value = Language.DE; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs new file mode 100644 index 000000000..e83ee7c60 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Squidex.Infrastructure +{ + public sealed class LanguagesInitializerTests + { + [Fact] + public async Task Should_add_custom_languages() + { + var options = Options.Create(new LanguagesOptions + { + ["en-NO"] = "English (Norwegian)" + }); + + var sut = new LanguagesInitializer(options); + + await sut.InitializeAsync(); + + Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); + } + + [Fact] + public async Task Should_not_add_invalid_languages() + { + var options = Options.Create(new LanguagesOptions + { + ["en-Error"] = null! + }); + + var sut = new LanguagesInitializer(options); + + await sut.InitializeAsync(); + + Assert.False(Language.TryGetLanguage("en-Error", out _)); + } + + [Fact] + public async Task Should_not_override_existing_languages() + { + var options = Options.Create(new LanguagesOptions + { + ["de"] = "German (Germany)" + }); + + var sut = new LanguagesInitializer(options); + + await sut.InitializeAsync(); + + Assert.Equal("German", Language.GetLanguage("de").EnglishName); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs new file mode 100644 index 000000000..a26190676 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Infrastructure.Log +{ + public class LockingLogStoreTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ILockGrain lockGrain = A.Fake(); + private readonly ILogStore inner = A.Fake(); + private readonly LockingLogStore sut; + + public LockingLogStoreTests() + { + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(lockGrain); + + sut = new LockingLogStore(inner, grainFactory); + } + + [Fact] + public async Task Should_lock_and_call_inner() + { + var stream = new MemoryStream(); + + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(2); + + var key = "MyKey"; + + var releaseToken = Guid.NewGuid().ToString(); + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .Returns(releaseToken); + + await sut.ReadLogAsync(key, dateFrom, dateTo, stream); + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .MustHaveHappened(); + + A.CallTo(() => lockGrain.ReleaseLockAsync(releaseToken)) + .MustHaveHappened(); + + A.CallTo(() => inner.ReadLogAsync(key, dateFrom, dateTo, stream)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_write_default_message_if_lock_could_not_be_acquired() + { + var stream = new MemoryStream(); + + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(2); + + var key = "MyKey"; + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .Returns(Task.FromResult(null)); + + await sut.ReadLogAsync(key, dateFrom, dateTo, stream, TimeSpan.FromSeconds(1)); + + A.CallTo(() => lockGrain.AcquireLockAsync(key)) + .MustHaveHappened(); + + A.CallTo(() => lockGrain.ReleaseLockAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => inner.ReadLogAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + + Assert.True(stream.Length > 0); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogAdapterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs new file mode 100644 index 000000000..7f00d407a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs @@ -0,0 +1,525 @@ +// ========================================================================== +// 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 FakeItEasy; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Infrastructure.Log.Adapter; +using Xunit; + +namespace Squidex.Infrastructure.Log +{ + public class SemanticLogTests + { + private readonly List appenders = new List(); + private readonly List channels = new List(); + private readonly Lazy log; + private readonly ILogChannel channel = A.Fake(); + private string output = string.Empty; + + public SemanticLog Log + { + get { return log.Value; } + } + + public SemanticLogTests() + { + channels.Add(channel); + + A.CallTo(() => channel.Log(A.Ignored, A.Ignored)) + .Invokes((SemanticLogLevel level, string message) => + { + output += message; + }); + + log = new Lazy(() => new SemanticLog(channels, appenders, JsonLogWriterFactory.Default())); + } + + [Fact] + public void Should_log_multiple_lines() + { + Log.Log(SemanticLogLevel.Error, None.Value, (_, w) => w.WriteProperty("logMessage", "Msg1")); + Log.Log(SemanticLogLevel.Error, None.Value, (_, w) => w.WriteProperty("logMessage", "Msg2")); + + var expected1 = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logMessage", "Msg1")); + + var expected2 = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logMessage", "Msg2")); + + Assert.Equal(expected1 + expected2, output); + } + + [Fact] + public void Should_log_timestamp() + { + var clock = A.Fake(); + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + + appenders.Add(new TimestampLogAppender(clock)); + + Log.LogFatal(w => { /* Do Nothing */ }); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("timestamp", clock.GetCurrentInstant())); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_values_with_appender() + { + appenders.Add(new ConstantsLogWriter(w => w.WriteProperty("logValue", 1500))); + + Log.LogFatal(m => { }); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_application_info() + { + var sessionId = Guid.NewGuid(); + + appenders.Add(new ApplicationInfoLogAppender(GetType(), sessionId)); + + Log.LogFatal(m => { }); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteObject("app", a => a + .WriteProperty("name", "Squidex.Infrastructure.Tests") + .WriteProperty("version", "1.0.0.0") + .WriteProperty("sessionId", sessionId.ToString()))); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_trace() + { + Log.LogTrace(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_trace_and_context() + { + Log.LogTrace(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_debug() + { + Log.LogDebug(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_debug_and_context() + { + Log.LogDebug(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_information() + { + Log.LogInformation(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_information_and_context() + { + Log.LogInformation(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning() + { + Log.LogWarning(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning_and_context() + { + Log.LogWarning(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning_exception() + { + var exception = new InvalidOperationException(); + + Log.LogWarning(exception); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_warning_exception_and_context() + { + var exception = new InvalidOperationException(); + + Log.LogWarning(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Warning") + .WriteProperty("logValue", 1500) + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error() + { + Log.LogError(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error_and_context() + { + Log.LogError(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error_exception() + { + var exception = new InvalidOperationException(); + + Log.LogError(exception); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_error_exception_and_context() + { + var exception = new InvalidOperationException(); + + Log.LogError(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Error") + .WriteProperty("logValue", 1500) + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal() + { + Log.LogFatal(w => w.WriteProperty("logValue", 1500)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal_and_context() + { + Log.LogFatal(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal_exception() + { + var exception = new InvalidOperationException(); + + Log.LogFatal(exception); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_with_fatal_exception_and_context() + { + var exception = new InvalidOperationException(); + + Log.LogFatal(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("logValue", 1500) + .WriteException(exception)); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_log_nothing_when_exception_is_null() + { + Log.LogFatal((Exception?)null); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal")); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_measure_trace() + { + Log.MeasureTrace(w => w.WriteProperty("message", "My Message")).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_trace_with_contex() + { + Log.MeasureTrace("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Trace") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_debug() + { + Log.MeasureDebug(w => w.WriteProperty("message", "My Message")).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_debug_with_contex() + { + Log.MeasureDebug("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Debug") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_information() + { + Log.MeasureInformation(w => w.WriteProperty("message", "My Message")).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_measure_information_with_contex() + { + Log.MeasureInformation("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Information") + .WriteProperty("message", "My Message") + .WriteProperty("elapsedMs", 0)); + + Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); + } + + [Fact] + public void Should_log_with_extensions_logger() + { + var exception = new InvalidOperationException(); + + var loggerFactory = + new LoggerFactory() + .AddSemanticLog(Log); + var loggerInstance = loggerFactory.CreateLogger(); + + loggerInstance.LogCritical(new EventId(123, "EventName"), exception, "Log {0}", 123); + + var expected = + LogTest(w => w + .WriteProperty("logLevel", "Fatal") + .WriteProperty("message", "Log 123") + .WriteObject("eventId", e => e + .WriteProperty("id", 123) + .WriteProperty("name", "EventName")) + .WriteException(exception) + .WriteProperty("category", "Squidex.Infrastructure.Log.SemanticLogTests")); + + Assert.Equal(expected, output); + } + + [Fact] + public void Should_catch_all_exceptions_from_all_channels_when_exceptions_are_thrown() + { + var exception1 = new InvalidOperationException(); + var exception2 = new InvalidOperationException(); + + var channel1 = A.Fake(); + var channel2 = A.Fake(); + + A.CallTo(() => channel1.Log(A.Ignored, A.Ignored)).Throws(exception1); + A.CallTo(() => channel2.Log(A.Ignored, A.Ignored)).Throws(exception2); + + var sut = new SemanticLog(new[] { channel1, channel2 }, Enumerable.Empty(), JsonLogWriterFactory.Default()); + + try + { + sut.Log(SemanticLogLevel.Debug, None.Value, (_, w) => w.WriteProperty("should", "throw")); + + Assert.False(true); + } + catch (AggregateException ex) + { + Assert.Equal(exception1, ex.InnerExceptions[0]); + Assert.Equal(exception2, ex.InnerExceptions[1]); + } + } + + private static string LogTest(Action writer) + { + var sut = JsonLogWriterFactory.Default().Create(); + + writer(sut); + + return sut.ToString(); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs new file mode 100644 index 000000000..674de68bd --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// 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 System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Migrations +{ + public class MigratorTests + { + private readonly IMigrationStatus status = A.Fake(); + private readonly IMigrationPath path = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); + + public sealed class InMemoryStatus : IMigrationStatus + { + private readonly object lockObject = new object(); + private int version; + private bool isLocked; + + public Task GetVersionAsync() + { + return Task.FromResult(version); + } + + public Task TryLockAsync() + { + var lockAcquired = false; + + lock (lockObject) + { + if (!isLocked) + { + isLocked = true; + + lockAcquired = true; + } + } + + return Task.FromResult(lockAcquired); + } + + public Task UnlockAsync(int newVersion) + { + lock (lockObject) + { + isLocked = false; + + version = newVersion; + } + + return TaskHelper.Done; + } + } + + public MigratorTests() + { + A.CallTo(() => path.GetNext(A.Ignored)) + .ReturnsLazily((int v) => + { + var m = migrations.Where(x => x.From == v).ToList(); + + return m.Count == 0 ? (0, null) : (migrations.Max(x => x.To), migrations.Select(x => x.Migration)); + }); + + A.CallTo(() => status.GetVersionAsync()).Returns(0); + A.CallTo(() => status.TryLockAsync()).Returns(true); + } + + [Fact] + public async Task Should_migrate_step_by_step() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var sut = new Migrator(status, path, log); + + await sut.MigrateAsync(); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); + + A.CallTo(() => status.UnlockAsync(3)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_unlock_when_migration_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var sut = new Migrator(status, path, log); + + A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); + + await Assert.ThrowsAsync(() => sut.MigrateAsync()); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); + + A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); + } + + [Fact] + public async Task Should_log_exception_when_migration_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + + var ex = new InvalidOperationException(); + + A.CallTo(() => migrator_0_1.UpdateAsync()) + .Throws(ex); + + var sut = new Migrator(status, path, log); + + await Assert.ThrowsAsync(() => sut.MigrateAsync()); + + A.CallTo(() => log.Log(SemanticLogLevel.Fatal, None.Value, A>.Ignored)) + .MustHaveHappened(); + + A.CallTo(() => migrator_1_2.UpdateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_prevent_multiple_updates() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + + var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; + + await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); + + A.CallTo(() => migrator_0_1.UpdateAsync()) + .MustHaveHappened(1, Times.Exactly); + A.CallTo(() => migrator_1_2.UpdateAsync()) + .MustHaveHappened(1, Times.Exactly); + } + + private IMigration BuildMigration(int fromVersion, int toVersion) + { + var migration = A.Fake(); + + migrations.Add((fromVersion, toVersion, migration)); + + return migration; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonConverterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs new file mode 100644 index 000000000..030a6fbb7 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs @@ -0,0 +1,169 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.MongoDb +{ + public class MongoExtensionsTests + { + public sealed class Cursor : IAsyncCursor where T : notnull + { + private readonly List items = new List(); + private int index = -1; + + public IEnumerable Current + { + get + { + if (items[index] is Exception ex) + { + throw ex; + } + + return Enumerable.Repeat((T)items[index], 1); + } + } + + public Cursor Add(params T[] newItems) + { + foreach (var item in newItems) + { + items.Add(item); + } + + return this; + } + + public Cursor Add(Exception ex) + { + items.Add(ex); + + return this; + } + + public void Dispose() + { + } + + public bool MoveNext(CancellationToken cancellationToken = default) + { + index++; + + return index < items.Count; + } + + public async Task MoveNextAsync(CancellationToken cancellationToken = default) + { + await Task.Delay(1, cancellationToken); + + return MoveNext(cancellationToken); + } + } + + [Fact] + public async Task Should_enumerate_over_items() + { + var result = new List(); + + var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5); + + await cursor.ForEachPipelineAsync(x => + { + result.Add(x); + return TaskHelper.Done; + }); + + Assert.Equal(new List { 0, 1, 2, 3, 4, 5 }, result); + } + + [Fact] + public async Task Should_break_when_cursor_failed() + { + var ex = new InvalidOperationException(); + + var result = new List(); + + using (var cursor = new Cursor().Add(0, 1, 2).Add(ex).Add(3, 4, 5)) + { + await Assert.ThrowsAsync(() => + { + return cursor.ForEachPipelineAsync(x => + { + result.Add(x); + return TaskHelper.Done; + }); + }); + } + + Assert.Equal(new List { 0, 1, 2 }, result); + } + + [Fact] + public async Task Should_break_when_handler_failed() + { + var ex = new InvalidOperationException(); + + var result = new List(); + + using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) + { + await Assert.ThrowsAsync(() => + { + return cursor.ForEachPipelineAsync(x => + { + if (x == 2) + { + throw ex; + } + + result.Add(x); + return TaskHelper.Done; + }); + }); + } + + Assert.Equal(new List { 0, 1 }, result); + } + + [Fact] + public async Task Should_stop_when_cancelled1() + { + using (var cts = new CancellationTokenSource()) + { + var result = new List(); + + using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) + { + await Assert.ThrowsAnyAsync(() => + { + return cursor.ForEachPipelineAsync(x => + { + if (x == 2) + { + cts.Cancel(); + } + + result.Add(x); + + return TaskHelper.Done; + }, cts.Token); + }); + } + + Assert.Equal(new List { 0, 1, 2 }, result); + } + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs new file mode 100644 index 000000000..add1d6d8e --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class NamedIdTests + { + [Fact] + public void Should_instantiate_token() + { + var id = Guid.NewGuid(); + + var namedId = NamedId.Of(id, "my-name"); + + Assert.Equal(id, namedId.Id); + Assert.Equal("my-name", namedId.Name); + } + + [Fact] + public void Should_convert_named_id_to_string() + { + var id = Guid.NewGuid(); + + var namedId = NamedId.Of(id, "my-name"); + + Assert.Equal($"{id},my-name", namedId.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var named_id1_name1_a = NamedId.Of(id1, "name1"); + var named_id1_name1_b = NamedId.Of(id1, "name1"); + + var named_id2_name1 = NamedId.Of(id2, "name1"); + var named_id1_name2 = NamedId.Of(id1, "name2"); + + Assert.Equal(named_id1_name1_a, named_id1_name1_b); + Assert.Equal(named_id1_name1_a.GetHashCode(), named_id1_name1_b.GetHashCode()); + Assert.True(named_id1_name1_a.Equals((object)named_id1_name1_b)); + + Assert.NotEqual(named_id1_name1_a, named_id2_name1); + Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id2_name1.GetHashCode()); + Assert.False(named_id1_name1_a.Equals((object)named_id2_name1)); + + Assert.NotEqual(named_id1_name1_a, named_id1_name2); + Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id1_name2.GetHashCode()); + Assert.False(named_id1_name1_a.Equals((object)named_id1_name2)); + } + + [Fact] + public void Should_serialize_and_deserialize_null_guid_token() + { + NamedId? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_guid_token() + { + var value = NamedId.Of(Guid.NewGuid(), "my-name"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_null_long_token() + { + NamedId? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_long_token() + { + var value = NamedId.Of(123L, "my-name"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_null_string_token() + { + NamedId? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_string_token() + { + var value = NamedId.Of(Guid.NewGuid().ToString(), "my-name"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_throw_exception_if_string_id_is_not_valid() + { + Assert.ThrowsAny(() => JsonHelper.Deserialize>("123")); + } + + [Fact] + public void Should_throw_exception_if_long_id_is_not_valid() + { + Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-long,name")); + } + + [Fact] + public void Should_throw_exception_if_guid_id_is_not_valid() + { + Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-guid,name")); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs new file mode 100644 index 000000000..16c4baa0c --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs @@ -0,0 +1,197 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexGrainTests + { + private readonly IGrainState> grainState = A.Fake>>(); + private readonly NamedId id1 = NamedId.Of(Guid.NewGuid(), "my-name1"); + private readonly NamedId id2 = NamedId.Of(Guid.NewGuid(), "my-name2"); + private readonly UniqueNameIndexGrain, Guid> sut; + + public UniqueNameIndexGrainTests() + { + A.CallTo(() => grainState.ClearAsync()) + .Invokes(() => grainState.Value = new UniqueNameIndexState()); + + sut = new UniqueNameIndexGrain, Guid>(grainState); + } + + [Fact] + public async Task Should_not_write_to_state_for_reservation() + { + await sut.ReserveAsync(id1.Id, id1.Name); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_to_index_if_reservation_token_acquired() + { + await AddAsync(id1); + + var result = await sut.GetIdAsync(id1.Name); + + Assert.Equal(id1.Id, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_make_reservation_if_name_already_reserved() + { + await sut.ReserveAsync(id1.Id, id1.Name); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.Null(newToken); + } + + [Fact] + public async Task Should_not_make_reservation_if_name_taken() + { + await AddAsync(id1); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.Null(newToken); + } + + [Fact] + public async Task Should_provide_number_of_entries() + { + await AddAsync(id1); + await AddAsync(id2); + + var count = await sut.CountAsync(); + + Assert.Equal(2, count); + } + + [Fact] + public async Task Should_clear_all_entries() + { + await AddAsync(id1); + await AddAsync(id2); + + await sut.ClearAsync(); + + var count = await sut.CountAsync(); + + Assert.Equal(0, count); + } + + [Fact] + public async Task Should_make_reservation_after_reservation_removed() + { + var token = await sut.ReserveAsync(id1.Id, id1.Name); + + await sut.RemoveReservationAsync(token!); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.NotNull(newToken); + } + + [Fact] + public async Task Should_make_reservation_after_id_removed() + { + await AddAsync(id1); + + await sut.RemoveAsync(id1.Id); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.NotNull(newToken); + } + + [Fact] + public async Task Should_remove_id_from_index() + { + await AddAsync(id1); + + await sut.RemoveAsync(id1.Id); + + var result = await sut.GetIdAsync(id1.Name); + + Assert.Equal(Guid.Empty, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_not_write_to_state_if_nothing_removed() + { + await sut.RemoveAsync(id1.Id); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_ignore_error_if_removing_reservation_with_Invalid_token() + { + await sut.RemoveReservationAsync(null); + } + + [Fact] + public async Task Should_ignore_error_if_completing_reservation_with_Invalid_token() + { + await sut.AddAsync(null!); + } + + [Fact] + public async Task Should_replace_ids_on_rebuild() + { + var state = new Dictionary + { + [id1.Name] = id1.Id, + [id2.Name] = id2.Id + }; + + await sut.RebuildAsync(state); + + Assert.Equal(id1.Id, await sut.GetIdAsync(id1.Name)); + Assert.Equal(id2.Id, await sut.GetIdAsync(id2.Name)); + + var result = await sut.GetIdsAsync(); + + Assert.Equal(new List { id1.Id, id2.Id }, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_provide_multiple_ids_by_names() + { + await AddAsync(id1); + await AddAsync(id2); + + var result = await sut.GetIdsAsync(new string[] { id1.Name, id2.Name, "not-found" }); + + Assert.Equal(new List { id1.Id, id2.Id }, result); + } + + private async Task AddAsync(NamedId id) + { + var token = await sut.ReserveAsync(id.Id, id.Name); + + await sut.AddAsync(token!); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs new file mode 100644 index 000000000..18058ebd8 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// JsonExternalSerializerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using FakeItEasy; +using Orleans.Serialization; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class JsonExternalSerializerTests + { + public JsonExternalSerializerTests() + { + J.DefaultSerializer = JsonHelper.DefaultSerializer; + } + + [Fact] + public void Should_not_copy_null() + { + var v = (string?)null; + var c = J.Copy(v, null); + + Assert.Null(c); + } + + [Fact] + public void Should_copy_null_json() + { + var v = new J?>(null); + var c = (J>)J.Copy(v, null)!; + + Assert.Null(c.Value); + } + + [Fact] + public void Should_not_copy_immutable_values() + { + var v = new List { 1, 2, 3 }.AsJ(); + var c = (J>)J.Copy(v, null)!; + + Assert.Same(v.Value, c.Value); + } + + [Fact] + public void Should_serialize_and_deserialize_value() + { + SerializeAndDeserialize(ArrayOfLength(100), Assert.Equal); + } + + [Fact] + public void Should_serialize_and_deserialize_large_value() + { + SerializeAndDeserialize(ArrayOfLength(8000), Assert.Equal); + } + + private static void SerializeAndDeserialize(T value, Action equals) where T : class + { + using (var buffer = new MemoryStream()) + { + J.Serialize(J.Of(value), CreateWriter(buffer), typeof(T)); + + buffer.Position = 0; + + var copy = (J)J.Deserialize(typeof(J), CreateReader(buffer))!; + + equals(copy.Value, value); + + Assert.NotSame(value, copy.Value); + } + } + + private static DeserializationContext CreateReader(MemoryStream buffer) + { + var reader = A.Fake(); + + A.CallTo(() => reader.ReadByteArray(A.Ignored, A.Ignored, A.Ignored)) + .Invokes(new Action((b, o, l) => buffer.Read(b, o, l))); + A.CallTo(() => reader.CurrentPosition) + .ReturnsLazily(x => (int)buffer.Position); + A.CallTo(() => reader.Length) + .ReturnsLazily(x => (int)buffer.Length); + + return new DeserializationContext(null) { StreamReader = reader }; + } + + private static SerializationContext CreateWriter(MemoryStream buffer) + { + var writer = A.Fake(); + + A.CallTo(() => writer.Write(A.Ignored, A.Ignored, A.Ignored)) + .Invokes(new Action(buffer.Write)); + A.CallTo(() => writer.CurrentOffset) + .ReturnsLazily(x => (int)buffer.Position); + + return new SerializationContext(null) { StreamWriter = writer }; + } + + private static List ArrayOfLength(int length) + { + var result = new List(); + + for (var i = 0; i < length; i++) + { + result.Add(i); + } + + return result; + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs new file mode 100644 index 000000000..2b101774c --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class LockGrainTests + { + private readonly LockGrain sut = new LockGrain(); + + [Fact] + public async Task Should_not_acquire_lock_when_locked() + { + var releaseLock1 = await sut.AcquireLockAsync("Key1"); + var releaseLock2 = await sut.AcquireLockAsync("Key1"); + + Assert.NotNull(releaseLock1); + Assert.Null(releaseLock2); + } + + [Fact] + public async Task Should_acquire_lock_with_other_key() + { + var releaseLock1 = await sut.AcquireLockAsync("Key1"); + var releaseLock2 = await sut.AcquireLockAsync("Key2"); + + Assert.NotNull(releaseLock1); + Assert.NotNull(releaseLock2); + } + + [Fact] + public async Task Should_acquire_lock_after_released() + { + var releaseLock1 = await sut.AcquireLockAsync("Key1"); + + await sut.ReleaseLockAsync(releaseLock1!); + + var releaseLock2 = await sut.AcquireLockAsync("Key1"); + + Assert.NotNull(releaseLock1); + Assert.NotNull(releaseLock2); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs new file mode 100644 index 000000000..0d7dc6ad3 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs @@ -0,0 +1,382 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class JsonQueryConversionTests + { + private readonly List errors = new List(); + private readonly JsonSchema schema = new JsonSchema(); + + public JsonQueryConversionTests() + { + var nested = new JsonSchemaProperty { Title = "nested" }; + + nested.Properties["property"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["boolean"] = new JsonSchemaProperty + { + Type = JsonObjectType.Boolean + }; + + schema.Properties["datetime"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime + }; + + schema.Properties["guid"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.Guid + }; + + schema.Properties["integer"] = new JsonSchemaProperty + { + Type = JsonObjectType.Integer + }; + + schema.Properties["number"] = new JsonSchemaProperty + { + Type = JsonObjectType.Number + }; + + schema.Properties["string"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["stringArray"] = new JsonSchemaProperty + { + Item = new JsonSchema + { + Type = JsonObjectType.String + }, + Type = JsonObjectType.Array + }; + + schema.Properties["object"] = nested; + + schema.Properties["reference"] = new JsonSchemaProperty + { + Reference = nested + }; + } + + [Fact] + public void Should_add_error_if_property_does_not_exist() + { + var json = new { path = "notfound", op = "eq", value = 1 }; + + AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); + } + + [Fact] + public void Should_add_error_if_nested_property_does_not_exist() + { + var json = new { path = "object.notfound", op = "eq", value = 1 }; + + AssertErrors(json, "'notfound' is not a property of 'nested'."); + } + + [Theory] + [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("empty", "empty(datetime)")] + [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] + [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] + [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] + [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] + [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] + [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] + [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] + public void Should_parse_datetime_string_filter(string op, string expected) + { + var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_parse_date_string_filter() + { + var json = new { path = "datetime", op = "eq", value = "2012-11-10" }; + + AssertFilter(json, "datetime == 2012-11-10T00:00:00Z"); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_string_value() + { + var json = new { path = "datetime", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_value() + { + var json = new { path = "datetime", op = "eq", value = 1 }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("empty", "empty(guid)")] + [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + public void Should_parse_guid_string_filter(string op, string expected) + { + var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_string_value() + { + var json = new { path = "guid", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_value() + { + var json = new { path = "guid", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(string, 'Hello')")] + [InlineData("empty", "empty(string)")] + [InlineData("endswith", "endsWith(string, 'Hello')")] + [InlineData("eq", "string == 'Hello'")] + [InlineData("ge", "string >= 'Hello'")] + [InlineData("gt", "string > 'Hello'")] + [InlineData("le", "string <= 'Hello'")] + [InlineData("lt", "string < 'Hello'")] + [InlineData("ne", "string != 'Hello'")] + [InlineData("startswith", "startsWith(string, 'Hello')")] + public void Should_parse_string_filter(string op, string expected) + { + var json = new { path = "string", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_string_property_got_invalid_value() + { + var json = new { path = "string", op = "eq", value = 1 }; + + AssertErrors(json, "Expected String for path 'string', but got Number."); + } + + [Fact] + public void Should_parse_string_in_filter() + { + var json = new { path = "string", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "string in ['Hello']"); + } + + [Fact] + public void Should_parse_nested_string_filter() + { + var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "object.property in ['Hello']"); + } + + [Fact] + public void Should_parse_referenced_string_filter() + { + var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "reference.property in ['Hello']"); + } + + [Theory] + [InlineData("eq", "number == 12")] + [InlineData("ge", "number >= 12")] + [InlineData("gt", "number > 12")] + [InlineData("le", "number <= 12")] + [InlineData("lt", "number < 12")] + [InlineData("ne", "number != 12")] + public void Should_parse_number_filter(string op, string expected) + { + var json = new { path = "number", op, value = 12 }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_number_property_got_invalid_value() + { + var json = new { path = "number", op = "eq", value = true }; + + AssertErrors(json, "Expected Number for path 'number', but got Boolean."); + } + + [Fact] + public void Should_parse_number_in_filter() + { + var json = new { path = "number", op = "in", value = new[] { 12 } }; + + AssertFilter(json, "number in [12]"); + } + + [Theory] + [InlineData("eq", "boolean == True")] + [InlineData("ne", "boolean != True")] + public void Should_parse_boolean_filter(string op, string expected) + { + var json = new { path = "boolean", op, value = true }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_boolean_property_got_invalid_value() + { + var json = new { path = "boolean", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); + } + + [Fact] + public void Should_parse_boolean_in_filter() + { + var json = new { path = "boolean", op = "in", value = new[] { true } }; + + AssertFilter(json, "boolean in [True]"); + } + + [Theory] + [InlineData("empty", "empty(stringArray)")] + [InlineData("eq", "stringArray == 'Hello'")] + [InlineData("ne", "stringArray != 'Hello'")] + public void Should_parse_array_filter(string op, string expected) + { + var json = new { path = "stringArray", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_parse_array_in_filter() + { + var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "stringArray in ['Hello']"); + } + + [Fact] + public void Should_add_error_when_using_array_value_for_non_allowed_operator() + { + var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; + + AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); + } + + [Fact] + public void Should_parse_query() + { + var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + + AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + } + + [Fact] + public void Should_parse_query_with_sorting() + { + var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; + + AssertQuery(json, "Sort: string Ascending"); + } + + [Fact] + public void Should_throw_exception_for_invalid_query() + { + var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; + + Assert.Throws(() => AssertQuery(json, null)); + } + + [Fact] + public void Should_throw_exception_when_parsing_invalid_json() + { + var json = "invalid"; + + Assert.Throws(() => AssertQuery(json, null)); + } + + private void AssertQuery(object json, string? expectedFilter) + { + var filter = ConvertQuery(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertFilter(object json, string? expectedFilter) + { + var filter = ConvertFilter(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertErrors(object json, params string[] expectedErrors) + { + var filter = ConvertFilter(json); + + Assert.Equal(expectedErrors.ToList(), errors); + + Assert.Null(filter); + } + + private string? ConvertFilter(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); + + return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); + } + + private string? ConvertQuery(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); + + return jsonFilter.ToString(); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs new file mode 100644 index 000000000..afe253aba --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class PascalCasePathConverterTests + { + [Fact] + public void Should_convert_property() + { + var source = ClrFilter.Eq("property", 1); + var result = PascalCasePathConverter.Transform(source); + + Assert.Equal("Property == 1", result!.ToString()); + } + + [Fact] + public void Should_convert_properties() + { + var source = ClrFilter.Eq("root.child", 1); + var result = PascalCasePathConverter.Transform(source); + + Assert.Equal("Root.Child == 1", result!.ToString()); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs new file mode 100644 index 000000000..be114009d --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs @@ -0,0 +1,374 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public sealed class QueryJsonConversionTests + { + private readonly List errors = new List(); + private readonly JsonSchema schema = new JsonSchema(); + + public QueryJsonConversionTests() + { + var nested = new JsonSchemaProperty { Title = "nested" }; + + nested.Properties["property"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["boolean"] = new JsonSchemaProperty + { + Type = JsonObjectType.Boolean + }; + + schema.Properties["datetime"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime + }; + + schema.Properties["guid"] = new JsonSchemaProperty + { + Type = JsonObjectType.String, Format = JsonFormatStrings.Guid + }; + + schema.Properties["integer"] = new JsonSchemaProperty + { + Type = JsonObjectType.Integer + }; + + schema.Properties["number"] = new JsonSchemaProperty + { + Type = JsonObjectType.Number + }; + + schema.Properties["string"] = new JsonSchemaProperty + { + Type = JsonObjectType.String + }; + + schema.Properties["stringArray"] = new JsonSchemaProperty + { + Item = new JsonSchema + { + Type = JsonObjectType.String + }, + Type = JsonObjectType.Array + }; + + schema.Properties["object"] = nested; + + schema.Properties["reference"] = new JsonSchemaProperty + { + Reference = nested + }; + } + + [Fact] + public void Should_add_error_if_property_does_not_exist() + { + var json = new { path = "notfound", op = "eq", value = 1 }; + + AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); + } + + [Fact] + public void Should_add_error_if_nested_property_does_not_exist() + { + var json = new { path = "object.notfound", op = "eq", value = 1 }; + + AssertErrors(json, "'notfound' is not a property of 'nested'."); + } + + [Theory] + [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("empty", "empty(datetime)")] + [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] + [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] + [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] + [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] + [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] + [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] + [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] + [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] + public void Should_parse_datetime_string_filter(string op, string expected) + { + var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_string_value() + { + var json = new { path = "datetime", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_datetime_string_property_got_invalid_value() + { + var json = new { path = "datetime", op = "eq", value = 1 }; + + AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("empty", "empty(guid)")] + [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] + [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] + public void Should_parse_guid_string_filter(string op, string expected) + { + var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_string_value() + { + var json = new { path = "guid", op = "eq", value = "invalid" }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); + } + + [Fact] + public void Should_add_error_if_guid_string_property_got_invalid_value() + { + var json = new { path = "guid", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); + } + + [Theory] + [InlineData("contains", "contains(string, 'Hello')")] + [InlineData("empty", "empty(string)")] + [InlineData("endswith", "endsWith(string, 'Hello')")] + [InlineData("eq", "string == 'Hello'")] + [InlineData("ge", "string >= 'Hello'")] + [InlineData("gt", "string > 'Hello'")] + [InlineData("le", "string <= 'Hello'")] + [InlineData("lt", "string < 'Hello'")] + [InlineData("ne", "string != 'Hello'")] + [InlineData("startswith", "startsWith(string, 'Hello')")] + public void Should_parse_string_filter(string op, string expected) + { + var json = new { path = "string", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_string_property_got_invalid_value() + { + var json = new { path = "string", op = "eq", value = 1 }; + + AssertErrors(json, "Expected String for path 'string', but got Number."); + } + + [Fact] + public void Should_parse_string_in_filter() + { + var json = new { path = "string", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "string in ['Hello']"); + } + + [Fact] + public void Should_parse_nested_string_filter() + { + var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "object.property in ['Hello']"); + } + + [Fact] + public void Should_parse_referenced_string_filter() + { + var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "reference.property in ['Hello']"); + } + + [Theory] + [InlineData("eq", "number == 12")] + [InlineData("ge", "number >= 12")] + [InlineData("gt", "number > 12")] + [InlineData("le", "number <= 12")] + [InlineData("lt", "number < 12")] + [InlineData("ne", "number != 12")] + public void Should_parse_number_filter(string op, string expected) + { + var json = new { path = "number", op, value = 12 }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_number_property_got_invalid_value() + { + var json = new { path = "number", op = "eq", value = true }; + + AssertErrors(json, "Expected Number for path 'number', but got Boolean."); + } + + [Fact] + public void Should_parse_number_in_filter() + { + var json = new { path = "number", op = "in", value = new[] { 12 } }; + + AssertFilter(json, "number in [12]"); + } + + [Theory] + [InlineData("eq", "boolean == True")] + [InlineData("ne", "boolean != True")] + public void Should_parse_boolean_filter(string op, string expected) + { + var json = new { path = "boolean", op, value = true }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_add_error_if_boolean_property_got_invalid_value() + { + var json = new { path = "boolean", op = "eq", value = 1 }; + + AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); + } + + [Fact] + public void Should_parse_boolean_in_filter() + { + var json = new { path = "boolean", op = "in", value = new[] { true } }; + + AssertFilter(json, "boolean in [True]"); + } + + [Theory] + [InlineData("empty", "empty(stringArray)")] + [InlineData("eq", "stringArray == 'Hello'")] + [InlineData("ne", "stringArray != 'Hello'")] + public void Should_parse_array_filter(string op, string expected) + { + var json = new { path = "stringArray", op, value = "Hello" }; + + AssertFilter(json, expected); + } + + [Fact] + public void Should_parse_array_in_filter() + { + var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; + + AssertFilter(json, "stringArray in ['Hello']"); + } + + [Fact] + public void Should_add_error_when_using_array_value_for_non_allowed_operator() + { + var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; + + AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); + } + + [Fact] + public void Should_parse_query() + { + var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + + AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + } + + [Fact] + public void Should_parse_query_with_sorting() + { + var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; + + AssertQuery(json, "Sort: string Ascending"); + } + + [Fact] + public void Should_throw_exception_for_invalid_query() + { + var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; + + Assert.Throws(() => AssertQuery(json, null)); + } + + [Fact] + public void Should_throw_exception_when_parsing_invalid_json() + { + var json = "invalid"; + + Assert.Throws(() => AssertQuery(json, null)); + } + + private void AssertQuery(object json, string? expectedFilter) + { + var filter = ConvertQuery(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertFilter(object json, string? expectedFilter) + { + var filter = ConvertFilter(json); + + Assert.Empty(errors); + + Assert.Equal(expectedFilter, filter); + } + + private void AssertErrors(object json, params string[] expectedErrors) + { + var filter = ConvertFilter(json); + + Assert.Equal(expectedErrors.ToList(), errors); + + Assert.Null(filter); + } + + private string? ConvertFilter(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); + + return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); + } + + private string? ConvertQuery(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); + + return jsonFilter.ToString(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs new file mode 100644 index 000000000..4347c3523 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs @@ -0,0 +1,424 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Infrastructure.Queries.OData; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class QueryODataConversionTests + { + private static readonly IEdmModel EdmModel; + + static QueryODataConversionTests() + { + var entityType = new EdmEntityType("Squidex", "Users"); + + entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date); + entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double); + entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("UserSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + EdmModel = model; + } + + [Fact] + public void Should_parse_query() + { + var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); + + Assert.NotNull(parser); + } + + [Fact] + public void Should_parse_filter_when_type_is_datetime() + { + var i = Q("$filter=created eq 1988-01-19T12:00:00Z"); + var o = C("Filter: created == 1988-01-19T12:00:00Z"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_datetime_list() + { + var i = Q("$filter=created in ('1988-01-19T12:00:00Z')"); + var o = C("Filter: created in [1988-01-19T12:00:00Z]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_date() + { + var i = Q("$filter=created eq 1988-01-19"); + var o = C("Filter: created == 1988-01-19T00:00:00Z"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_date_list() + { + var i = Q("$filter=created in ('1988-01-19')"); + var o = C("Filter: created in [1988-01-19T00:00:00Z]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_guid() + { + var i = Q("$filter=id eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); + var o = C("Filter: id == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_guid_list() + { + var i = Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); + var o = C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_null() + { + var i = Q("$filter=firstName eq null"); + var o = C("Filter: firstName == null"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_string() + { + var i = Q("$filter=firstName eq 'Dagobert'"); + var o = C("Filter: firstName == 'Dagobert'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_string_list() + { + var i = Q("$filter=firstName in ('Dagobert')"); + var o = C("Filter: firstName in ['Dagobert']"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_boolean() + { + var i = Q("$filter=isComicFigure eq true"); + var o = C("Filter: isComicFigure == True"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_boolean_list() + { + var i = Q("$filter=isComicFigure in (true)"); + var o = C("Filter: isComicFigure in [True]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int32() + { + var i = Q("$filter=age eq 60"); + var o = C("Filter: age == 60"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int32_list() + { + var i = Q("$filter=age in (60)"); + var o = C("Filter: age in [60]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int64() + { + var i = Q("$filter=incomeCents eq 31543143513456789"); + var o = C("Filter: incomeCents == 31543143513456789"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_int64_list() + { + var i = Q("$filter=incomeCents in (31543143513456789)"); + var o = C("Filter: incomeCents in [31543143513456789]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_double() + { + var i = Q("$filter=incomeMio eq 5634474356.1233"); + var o = C("Filter: incomeMio == 5634474356.1233"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_when_type_is_double_list() + { + var i = Q("$filter=incomeMio in (5634474356.1233)"); + var o = C("Filter: incomeMio in [5634474356.1233]"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_negation() + { + var i = Q("$filter=not endswith(lastName, 'Duck')"); + var o = C("Filter: !(endsWith(lastName, 'Duck'))"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_startswith() + { + var i = Q("$filter=startswith(lastName, 'Duck')"); + var o = C("Filter: startsWith(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_endswith() + { + var i = Q("$filter=endswith(lastName, 'Duck')"); + var o = C("Filter: endsWith(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_empty() + { + var i = Q("$filter=empty(lastName)"); + var o = C("Filter: empty(lastName)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_empty_to_true() + { + var i = Q("$filter=empty(lastName) eq true"); + var o = C("Filter: empty(lastName)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains() + { + var i = Q("$filter=contains(lastName, 'Duck')"); + var o = C("Filter: contains(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains_to_true() + { + var i = Q("$filter=contains(lastName, 'Duck') eq true"); + var o = C("Filter: contains(lastName, 'Duck')"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_contains_to_false() + { + var i = Q("$filter=contains(lastName, 'Duck') eq false"); + var o = C("Filter: !(contains(lastName, 'Duck'))"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_equals() + { + var i = Q("$filter=age eq 1"); + var o = C("Filter: age == 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_notequals() + { + var i = Q("$filter=age ne 1"); + var o = C("Filter: age != 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_lessthan() + { + var i = Q("$filter=age lt 1"); + var o = C("Filter: age < 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_lessthanorequal() + { + var i = Q("$filter=age le 1"); + var o = C("Filter: age <= 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_greaterthan() + { + var i = Q("$filter=age gt 1"); + var o = C("Filter: age > 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_greaterthanorequal() + { + var i = Q("$filter=age ge 1"); + var o = C("Filter: age >= 1"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_conjunction_and_contains() + { + var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); + var o = C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_conjunction() + { + var i = Q("$filter=age eq 1 and age eq 2"); + var o = C("Filter: (age == 1 && age == 2)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_disjunction() + { + var i = Q("$filter=age eq 1 or age eq 2"); + var o = C("Filter: (age == 1 || age == 2)"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text_numbers() + { + var i = Q("$search=\"33k\""); + var o = C("FullText: '33k'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text() + { + var i = Q("$search=Duck"); + var o = C("FullText: 'Duck'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_with_full_text_and_multiple_terms() + { + var i = Q("$search=Dagobert or Donald"); + var o = C("FullText: 'Dagobert or Donald'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var i = Q("$orderby=age desc"); + var o = C("Sort: age Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_make_orderby_with_multiple_field() + { + var i = Q("$orderby=age, incomeMio desc"); + var o = C("Sort: age Ascending, incomeMio Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_filter_and_take() + { + var i = Q("$top=3&$skip=4"); + var o = C("Skip: 4; Take: 3"); + + Assert.Equal(o, i); + } + + private static string C(string value) + { + return value; + } + + private static string? Q(string value) + { + var parser = EdmModel.ParseQuery(value); + + return parser?.ToQuery().ToString(); + } + } +} \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs new file mode 100644 index 000000000..47a5ba1f5 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs @@ -0,0 +1,94 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class QueryOptimizationTests + { + [Fact] + public void Should_not_convert_optimize_valid_logical_filter() + { + var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("(path == 2 || path == 3)", result!.ToString()); + } + + [Fact] + public void Should_return_filter_When_logical_filter_has_one_child() + { + var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); + + var result = Optimizer.Optimize(source); + + Assert.Equal("path == 1", result!.ToString()); + } + + [Fact] + public void Should_return_null_when_filters_of_logical_filter_get_optimized_away() + { + var source = ClrFilter.And(ClrFilter.And()); + + var result = Optimizer.Optimize(source); + + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_logical_filter_has_no_filter() + { + var source = ClrFilter.And(); + + var result = Optimizer.Optimize(source); + + Assert.Null(result); + } + + [Fact] + public void Should_return_null_when_filter_of_negation_get_optimized_away() + { + var source = ClrFilter.Not(ClrFilter.And()); + + var result = Optimizer.Optimize(source); + + Assert.Null(result); + } + + [Fact] + public void Should_invert_equals_not_filter() + { + var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("path != 1", result!.ToString()); + } + + [Fact] + public void Should_invert_notequals_not_filter() + { + var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("path == 1", result!.ToString()); + } + + [Fact] + public void Should_not_convert_number_operator() + { + var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); + + var result = Optimizer.Optimize(source); + + Assert.Equal("!(path < 1)", result!.ToString()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/RandomHashTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs new file mode 100644 index 000000000..9374b9845 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -0,0 +1,122 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class RefTokenTests + { + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(":")] + [InlineData("user")] + public void Should_throw_exception_if_parsing_invalid_input(string input) + { + Assert.Throws(() => RefToken.Parse(input)); + } + + [Fact] + public void Should_instantiate_token() + { + var token = new RefToken("client", "client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + + Assert.True(token.IsClient); + } + + [Fact] + public void Should_instantiate_subject_token() + { + var token = new RefToken("subject", "client1"); + + Assert.True(token.IsSubject); + } + + [Fact] + public void Should_instantiate_token_and_lower_type() + { + var token = new RefToken("Client", "client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + + [Fact] + public void Should_parse_user_token_from_string() + { + var token = RefToken.Parse("client:client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + + [Fact] + public void Should_parse_user_token_with_colon_in_identifier() + { + var token = RefToken.Parse("client:client1:app"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1:app", token.Identifier); + } + + [Fact] + public void Should_convert_user_token_to_string() + { + var token = RefToken.Parse("client:client1"); + + Assert.Equal("client:client1", token.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var token_type1_id1_a = RefToken.Parse("type1:client1"); + var token_type1_id1_b = RefToken.Parse("type1:client1"); + + var token_type2_id1 = RefToken.Parse("type2:client1"); + var token_type1_id2 = RefToken.Parse("type1:client2"); + + Assert.Equal(token_type1_id1_a, token_type1_id1_b); + Assert.Equal(token_type1_id1_a.GetHashCode(), token_type1_id1_b.GetHashCode()); + Assert.True(token_type1_id1_a.Equals((object)token_type1_id1_b)); + + Assert.NotEqual(token_type1_id1_a, token_type2_id1); + Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type2_id1.GetHashCode()); + Assert.False(token_type1_id1_a.Equals((object)token_type2_id1)); + + Assert.NotEqual(token_type1_id1_a, token_type1_id2); + Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type1_id2.GetHashCode()); + Assert.False(token_type1_id1_a.Equals((object)token_type1_id2)); + } + + [Fact] + public void Should_serialize_and_deserialize_null_token() + { + RefToken? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_token() + { + var value = RefToken.Parse("client:client1"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs new file mode 100644 index 000000000..f951970db --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs @@ -0,0 +1,177 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Diagnostics; +using Xunit; + +namespace Squidex.Infrastructure.Reflection +{ + public class SimpleMapperTests + { + public class Class1Base + { + public T1 P1 { get; set; } + } + + public class Class1 : Class1Base + { + public T2 P2 { get; set; } + } + + public class Class2Base + { + public T2 P2 { get; set; } + } + + public class Class2 : Class2Base + { + public T3 P3 { get; set; } + } + + public class Readonly + { + public T P1 { get; } + } + + public class Writeonly + { + public T P1 + { + set { Debug.WriteLine(value); } + } + } + + [Fact] + public void Should_throw_exception_if_mapping_with_null_source() + { + Assert.Throws(() => SimpleMapper.Map((Class2?)null!, new Class2())); + } + + [Fact] + public void Should_throw_exception_if_mapping_with_null_target() + { + Assert.Throws(() => SimpleMapper.Map(new Class2(), (Class2?)null!)); + } + + [Fact] + public void Should_map_to_same_type() + { + var obj1 = new Class1 + { + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_map_all_properties() + { + var obj1 = new Class1 + { + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class1()); + + Assert.Equal(6, obj2.P1); + Assert.Equal(8, obj2.P2); + } + + [Fact] + public void Should_map_to_convertible_type() + { + var obj1 = new Class1 + { + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_map_nullables() + { + var obj1 = new Class1 + { + P1 = true, + P2 = true + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.True(obj2.P2); + Assert.False(obj2.P3); + } + + [Fact] + public void Should_map_when_convertible_is_null() + { + var obj1 = new Class1 + { + P1 = null, + P2 = null + }; + var obj2 = SimpleMapper.Map(obj1, new Class1()); + + Assert.Equal(0, obj2.P1); + Assert.Equal(0, obj2.P2); + } + + [Fact] + public void Should_convert_to_string() + { + var obj1 = new Class1 + { + P1 = new RefToken("user", "1"), + P2 = new RefToken("user", "2") + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal("user:2", obj2.P2); + Assert.Null(obj2.P3); + } + + [Fact] + public void Should_return_default_if_conversion_failed() + { + var obj1 = new Class1 + { + P1 = long.MaxValue, + P2 = long.MaxValue + }; + var obj2 = SimpleMapper.Map(obj1, new Class2()); + + Assert.Equal(0, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_ignore_write_only() + { + var obj1 = new Writeonly(); + var obj2 = SimpleMapper.Map(obj1, new Class1()); + + Assert.Equal(0, obj2.P1); + } + + [Fact] + public void Should_ignore_read_only() + { + var obj1 = new Class1 { P1 = 10 }; + var obj2 = SimpleMapper.Map(obj1, new Readonly()); + + Assert.Equal(0, obj2.P1); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs new file mode 100644 index 000000000..d41c32120 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.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.Security.Claims; +using Xunit; + +namespace Squidex.Infrastructure.Security +{ + public class ExtensionsTests + { + [Fact] + public void Should_retrieve_subject() + { + TestClaimExtension(OpenIdClaims.Subject, x => x.OpenIdSubject()); + } + + [Fact] + public void Should_retrieve_client_id() + { + TestClaimExtension(OpenIdClaims.ClientId, x => x.OpenIdClientId()); + } + + [Fact] + public void Should_retrieve_preferred_user_name() + { + TestClaimExtension(OpenIdClaims.PreferredUserName, x => x.OpenIdPreferredUserName()); + } + + [Fact] + public void Should_retrieve_name() + { + TestClaimExtension(OpenIdClaims.Name, x => x.OpenIdName()); + } + + [Fact] + public void Should_retrieve_nickname() + { + TestClaimExtension(OpenIdClaims.NickName, x => x.OpenIdNickName()); + } + + [Fact] + public void Should_retrieve_email() + { + TestClaimExtension(OpenIdClaims.Email, x => x.OpenIdEmail()); + } + + private static void TestClaimExtension(string claimType, Func getter) + { + var claimValue = Guid.NewGuid().ToString(); + + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(); + + claimsIdentity.AddClaim(new Claim(claimType, claimValue)); + + Assert.Null(getter(claimsPrincipal)); + + claimsPrincipal.AddIdentity(claimsIdentity); + + Assert.Equal(claimValue, getter(claimsPrincipal)); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj new file mode 100644 index 000000000..9d268d3f0 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -0,0 +1,46 @@ + + + Exe + netcoreapp3.0 + Squidex.Infrastructure + 8.0 + enable + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + + + + + \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs new file mode 100644 index 000000000..f91f6e3be --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.States +{ + public class InconsistentStateExceptionTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); + var result = source.SerializeAndDeserializeBinary(); + + Assert.IsType(result.InnerException); + + Assert.Equal("Inner", result.InnerException?.Message); + + Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); + Assert.Equal(result.CurrentVersion, source.CurrentVersion); + + Assert.Equal(result.Message, source.Message); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs new file mode 100644 index 000000000..d4180f81b --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.TestHelpers +{ + public static class JsonHelper + { + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + + public static IJsonSerializer CreateSerializer(TypeNameRegistry? typeNameRegistry = null) + { + var serializerSettings = DefaultSettings(typeNameRegistry); + + return new NewtonsoftJsonSerializer(serializerSettings); + } + + public static JsonSerializerSettings DefaultSettings(TypeNameRegistry? typeNameRegistry = null) + { + return new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), + + ContractResolver = new ConverterContractResolver( + new ClaimsPrincipalConverter(), + new InstantConverter(), + new EnvelopeHeadersConverter(), + new FilterConverter(), + new JsonValueConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertyPathConverter(), + new RefTokenConverter(), + new StringEnumConverter()), + + TypeNameHandling = TypeNameHandling.Auto + }; + } + + public static T SerializeAndDeserialize(this T value) + { + return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; + } + + public static T Deserialize(string value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; + } + + public static T Deserialize(object value) + { + return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs new file mode 100644 index 000000000..f8c1593f8 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public sealed class MyDomainObject : DomainObjectGrain + { + public MyDomainObject(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return Create(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturn(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return Update(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturn(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + } + + public sealed class CreateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class CreateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateCustom : MyCommand + { + public int Value { get; set; } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs rename to backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs new file mode 100644 index 000000000..56cefc7d7 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public class MyGrain : DomainObjectGrain + { + public MyGrain(IStore store) + : base(store, A.Dummy()) + { + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + return Task.FromResult(null); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs new file mode 100644 index 000000000..61096410c --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -0,0 +1,228 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Infrastructure.Log; +using Xunit; + +namespace Squidex.Infrastructure.UsageTracking +{ + public class BackgroundUsageTrackerTests + { + private readonly IUsageRepository usageStore = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly string key = Guid.NewGuid().ToString(); + private readonly BackgroundUsageTracker sut; + + public BackgroundUsageTrackerTests() + { + sut = new BackgroundUsageTracker(usageStore, log); + } + + [Fact] + public async Task Should_throw_exception_if_tracking_on_disposed_object() + { + sut.Dispose(); + + await Assert.ThrowsAsync(() => sut.TrackAsync(key, "category1", 1, 1000)); + } + + [Fact] + public async Task Should_throw_exception_if_querying_on_disposed_object() + { + sut.Dispose(); + + await Assert.ThrowsAsync(() => sut.QueryAsync(key, DateTime.Today, DateTime.Today.AddDays(1))); + } + + [Fact] + public async Task Should_throw_exception_if_querying_montly_usage_on_disposed_object() + { + sut.Dispose(); + + await Assert.ThrowsAsync(() => sut.GetMonthlyCallsAsync(key, DateTime.Today)); + } + + [Fact] + public async Task Should_sum_up_when_getting_monthly_calls() + { + var date = new DateTime(2016, 1, 15); + + IReadOnlyList originalData = new List + { + new StoredUsage("category1", date.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", date.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", date.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 15))) + .Returns(originalData); + + var result = await sut.GetMonthlyCallsAsync(key, date); + + Assert.Equal(55, result); + } + + [Fact] + public async Task Should_sum_up_when_getting_last_calls_calls() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(10); + + IReadOnlyList originalData = new List + { + new StoredUsage("category1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", f.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", f.AddDays(7), Counters(17, 22)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(originalData); + + var result = await sut.GetPreviousCallsAsync(key, f, t); + + Assert.Equal(55, result); + } + + [Fact] + public async Task Should_fill_missing_days() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(4); + + var originalData = new List + { + new StoredUsage("MyCategory1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("MyCategory1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("MyCategory1", f.AddDays(4), Counters(15, 20)), + new StoredUsage(null, f.AddDays(0), Counters(17, 22)), + new StoredUsage(null, f.AddDays(2), Counters(11, 14)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(originalData); + + var result = await sut.QueryAsync(key, f, t); + + var expected = new Dictionary> + { + ["MyCategory1"] = new List + { + new DateUsage(f.AddDays(0), 00, 00), + new DateUsage(f.AddDays(1), 10, 15), + new DateUsage(f.AddDays(2), 00, 00), + new DateUsage(f.AddDays(3), 13, 18), + new DateUsage(f.AddDays(4), 15, 20) + }, + ["*"] = new List + { + new DateUsage(f.AddDays(0), 17, 22), + new DateUsage(f.AddDays(1), 00, 00), + new DateUsage(f.AddDays(2), 11, 14), + new DateUsage(f.AddDays(3), 00, 00), + new DateUsage(f.AddDays(4), 00, 00) + } + }; + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_fill_missing_days_with_star() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(4); + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(new List()); + + var result = await sut.QueryAsync(key, f, t); + + var expected = new Dictionary> + { + ["*"] = new List + { + new DateUsage(f.AddDays(0), 00, 00), + new DateUsage(f.AddDays(1), 00, 00), + new DateUsage(f.AddDays(2), 00, 00), + new DateUsage(f.AddDays(3), 00, 00), + new DateUsage(f.AddDays(4), 00, 00) + } + }; + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_not_track_if_weight_less_than_zero() + { + await sut.TrackAsync(key, "MyCategory", -1, 1000); + await sut.TrackAsync(key, "MyCategory", 0, 1000); + + sut.Next(); + sut.Dispose(); + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_aggregate_and_store_on_dispose() + { + var key1 = Guid.NewGuid().ToString(); + var key2 = Guid.NewGuid().ToString(); + var key3 = Guid.NewGuid().ToString(); + + var today = DateTime.Today; + + await sut.TrackAsync(key1, "MyCategory1", 1, 1000); + + await sut.TrackAsync(key2, "MyCategory1", 1.0, 2000); + await sut.TrackAsync(key2, "MyCategory1", 0.5, 3000); + + await sut.TrackAsync(key3, "MyCategory1", 0.3, 4000); + await sut.TrackAsync(key3, "MyCategory1", 0.1, 5000); + + await sut.TrackAsync(key3, null, 0.5, 2000); + await sut.TrackAsync(key3, null, 0.5, 6000); + + UsageUpdate[]? updates = null; + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .Invokes((UsageUpdate[] u) => updates = u); + + sut.Next(); + sut.Dispose(); + + updates.Should().BeEquivalentTo(new[] + { + new UsageUpdate(today, $"{key1}_API", "MyCategory1", Counters(1.0, 1000)), + new UsageUpdate(today, $"{key2}_API", "MyCategory1", Counters(1.5, 5000)), + new UsageUpdate(today, $"{key3}_API", "MyCategory1", Counters(0.4, 9000)), + new UsageUpdate(today, $"{key3}_API", "*", Counters(1, 8000)) + }, o => o.ComparingByMembers()); + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .MustHaveHappened(); + } + + private static Counters Counters(double count, long ms) + { + return new Counters + { + [BackgroundUsageTracker.CounterTotalCalls] = count, + [BackgroundUsageTracker.CounterTotalElapsedMs] = ms + }; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs diff --git a/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs new file mode 100644 index 000000000..b6388cdd8 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FluentAssertions; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class ValidationExceptionTests + { + [Fact] + public void Should_format_message_from_summary() + { + var ex = new ValidationException("Summary."); + + Assert.Equal("Summary.", ex.Message); + } + + [Fact] + public void Should_append_dot_to_summary() + { + var ex = new ValidationException("Summary"); + + Assert.Equal("Summary.", ex.Message); + } + + [Fact] + public void Should_format_message_from_errors() + { + var ex = new ValidationException("Summary", new ValidationError("Error1."), new ValidationError("Error2.")); + + Assert.Equal("Summary: Error1. Error2.", ex.Message); + } + + [Fact] + public void Should_not_add_colon_twice() + { + var ex = new ValidationException("Summary:", new ValidationError("Error1."), new ValidationError("Error2.")); + + Assert.Equal("Summary: Error1. Error2.", ex.Message); + } + + [Fact] + public void Should_append_dots_to_errors() + { + var ex = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); + + Assert.Equal("Summary: Error1. Error2.", ex.Message); + } + + [Fact] + public void Should_serialize_and_deserialize1() + { + var source = new ValidationException("Summary", new ValidationError("Error1"), null!); + var result = source.SerializeAndDeserializeBinary(); + + result.Errors.Should().BeEquivalentTo(source.Errors); + + Assert.Equal(source.Message, result.Message); + Assert.Equal(source.Summary, result.Summary); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); + var result = source.SerializeAndDeserializeBinary(); + + result.Errors.Should().BeEquivalentTo(source.Errors); + + Assert.Equal(source.Message, result.Message); + Assert.Equal(source.Summary, result.Summary); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs similarity index 100% rename from tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs diff --git a/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs rename to backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs new file mode 100644 index 000000000..72c539c26 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -0,0 +1,123 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Web +{ + public class ApiExceptionFilterAttributeTests + { + private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); + + [Fact] + public void Should_generate_404_for_DomainObjectNotFoundException() + { + var context = E(new DomainObjectNotFoundException("1", typeof(object))); + + sut.OnException(context); + + Assert.IsType(context.Result); + } + + [Fact] + public void Should_generate_400_for_ValidationException() + { + var ex = new ValidationException("NotAllowed", + new ValidationError("Error1"), + new ValidationError("Error2", "P"), + new ValidationError("Error3", "P1", "P2")); + + var context = E(ex); + + sut.OnException(context); + + var result = (ObjectResult)context.Result!; + + Assert.Equal(400, result.StatusCode); + Assert.Equal(400, (result.Value as ErrorDto)?.StatusCode); + + Assert.Equal(ex.Summary, (result.Value as ErrorDto)!.Message); + + Assert.Equal(new[] { "Error1", "P: Error2", "P1, P2: Error3" }, (result.Value as ErrorDto)!.Details); + } + + [Fact] + public void Should_generate_400_for_DomainException() + { + var context = E(new DomainException("NotAllowed")); + + sut.OnException(context); + + Validate(400, context); + } + + [Fact] + public void Should_generate_412_for_DomainObjectVersionException() + { + var context = E(new DomainObjectVersionException("1", typeof(object), 1, 2)); + + sut.OnException(context); + + Validate(412, context); + } + + [Fact] + public void Should_generate_403_for_DomainForbiddenException() + { + var context = E(new DomainForbiddenException("Forbidden")); + + sut.OnException(context); + + Validate(403, context); + } + + [Fact] + public void Should_generate_403_for_SecurityException() + { + var context = E(new SecurityException("Forbidden")); + + sut.OnException(context); + + Validate(403, context); + } + + private static ExceptionContext E(Exception exception) + { + var httpContext = new DefaultHttpContext(); + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + FilterDescriptors = new List() + }); + + return new ExceptionContext(actionContext, new List()) + { + Exception = exception + }; + } + + private static void Validate(int statusCode, ExceptionContext context) + { + var result = (ObjectResult)context.Result!; + + Assert.Equal(statusCode, result.StatusCode); + Assert.Equal(statusCode, (result.Value as ErrorDto)?.StatusCode); + + Assert.Equal(context.Exception.Message, (result.Value as ErrorDto)!.Message); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs new file mode 100644 index 000000000..b647d275d --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Web +{ + public class ApiPermissionAttributeTests + { + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private bool isNextCalled; + + public ApiPermissionAttributeTests() + { + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + FilterDescriptors = new List() + }); + + actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + } + + [Fact] + public void Should_use_bearer_schemes() + { + var sut = new ApiPermissionAttribute(); + + Assert.Equal("Bearer", sut.AuthenticationSchemes); + } + + [Fact] + public async Task Should_call_next_when_user_has_correct_permission() + { + actionExecutingContext.RouteData.Values["app"] = "my-app"; + + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Null(actionExecutingContext.Result); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_return_forbidden_when_user_has_wrong_permission() + { + actionExecutingContext.RouteData.Values["app"] = "my-app"; + + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_return_forbidden_when_route_data_has_no_value() + { + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_return_forbidden_when_user_has_no_permission() + { + var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs new file mode 100644 index 000000000..98729e997 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class ETagCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ETagCommandMiddleware sut; + + public ETagCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + sut = new ETagCommandMiddleware(httpContextAccessor); + } + + [Fact] + public async Task Should_do_nothing_when_context_is_null() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null!); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Null(command.Actor); + } + + [Fact] + public async Task Should_do_nothing_if_command_has_etag_defined() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; + + var command = new CreateContent { ExpectedVersion = 1 }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(1, context.Command.ExpectedVersion); + } + + [Fact] + public async Task Should_add_expected_version_to_command() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(13, context.Command.ExpectedVersion); + } + + [Fact] + public async Task Should_add_weak_etag_as_expected_version_to_command() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(13, context.Command.ExpectedVersion); + } + + [Fact] + public async Task Should_add_version_from_result_as_etag_to_response() + { + var command = new CreateContent(); + var context = Ctx(command); + + context.Complete(new EntitySavedResult(17)); + + await sut.HandleAsync(context); + + Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_add_version_from_entity_as_etag_to_response() + { + var command = new CreateContent(); + var context = Ctx(command); + + context.Complete(new ContentEntity { Version = 17 }); + + await sut.HandleAsync(context); + + Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs new file mode 100644 index 000000000..ec4bc4794 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Security; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithActorCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly EnrichWithActorCommandMiddleware sut; + + public EnrichWithActorCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + sut = new EnrichWithActorCommandMiddleware(httpContextAccessor); + } + + [Fact] + public async Task Should_throw_security_exception_when_no_subject_or_client_is_found() + { + var command = new CreateContent(); + var context = Ctx(command); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + + [Fact] + public async Task Should_do_nothing_when_context_is_null() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null!); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Null(command.Actor); + } + + [Fact] + public async Task Should_assign_actor_from_subject() + { + httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "me"); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(new RefToken(RefTokenType.Subject, "me"), command.Actor); + } + + [Fact] + public async Task Should_assign_actor_from_client() + { + httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); + + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(new RefToken(RefTokenType.Client, "my-client"), command.Actor); + } + + [Fact] + public async Task Should_not_override_actor() + { + httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); + + var command = new CreateContent { Actor = new RefToken("subject", "me") }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(new RefToken("subject", "me"), command.Actor); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + + private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue) + { + var claimsPrincipal = new ClaimsPrincipal(); + var claimsIdentity = new ClaimsIdentity(); + + claimsIdentity.AddClaim(new Claim(claimType, claimValue)); + claimsPrincipal.AddIdentity(claimsIdentity); + + return claimsPrincipal; + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..b8f4017fd --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithAppIdCommandMiddlewareTests + { + private readonly IContextProvider contextProvider = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly Context requestContext = Context.Anonymous(); + private readonly EnrichWithAppIdCommandMiddleware sut; + + public EnrichWithAppIdCommandMiddlewareTests() + { + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); + + var app = A.Fake(); + + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + + requestContext.App = app; + + sut = new EnrichWithAppIdCommandMiddleware(contextProvider); + } + + [Fact] + public async Task Should_throw_exception_if_app_not_found() + { + requestContext.App = null!; + + var command = new CreateContent(); + var context = Ctx(command); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + + [Fact] + public async Task Should_assign_app_id_and_name_to_app_command() + { + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(appId, command.AppId); + } + + [Fact] + public async Task Should_assign_app_id_to_app_self_command() + { + var command = new ChangePlan(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(appId.Id, command.AppId); + } + + [Fact] + public async Task Should_not_override_app_id() + { + var command = new ChangePlan { AppId = Guid.NewGuid() }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(appId.Id, command.AppId); + } + + [Fact] + public async Task Should_not_override_app_id_and_name() + { + var command = new CreateContent { AppId = NamedId.Of(Guid.NewGuid(), "other-app") }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(appId, command.AppId); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..5328eb81e --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -0,0 +1,157 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithSchemaIdCommandMiddlewareTests + { + private readonly IActionContextAccessor actionContextAccessor = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext = new ActionContext(); + private readonly EnrichWithSchemaIdCommandMiddleware sut; + + public EnrichWithSchemaIdCommandMiddlewareTests() + { + actionContext.RouteData = new RouteData(); + actionContext.HttpContext = httpContext; + + A.CallTo(() => actionContextAccessor.ActionContext) + .Returns(actionContext); + + var app = A.Fake(); + + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + + httpContext.Context().App = app; + + var schema = A.Fake(); + + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) + .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + + sut = new EnrichWithSchemaIdCommandMiddleware(appProvider, actionContextAccessor); + } + + [Fact] + public async Task Should_throw_exception_if_schema_not_found() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, "other-schema")) + .Returns(Task.FromResult(null)); + + actionContext.RouteData.Values["name"] = "other-schema"; + + var command = new CreateContent { AppId = appId }; + var context = Ctx(command); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + } + + [Fact] + public async Task Should_do_nothing_when_route_has_no_parameter() + { + var command = new CreateContent(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Null(command.Actor); + } + + [Fact] + public async Task Should_assign_schema_id_and_name_from_name() + { + actionContext.RouteData.Values["name"] = schemaId.Name; + + var command = new CreateContent { AppId = appId }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(schemaId, command.SchemaId); + } + + [Fact] + public async Task Should_assign_schema_id_and_name_from_id() + { + actionContext.RouteData.Values["name"] = schemaId.Id; + + var command = new CreateContent { AppId = appId }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(schemaId, command.SchemaId); + } + + [Fact] + public async Task Should_assign_schema_id_from_id() + { + actionContext.RouteData.Values["name"] = schemaId.Name; + + var command = new UpdateSchema(); + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.Equal(schemaId.Id, command.SchemaId); + } + + [Fact] + public async Task Should_not_override_schema_id() + { + var command = new CreateSchema { SchemaId = Guid.NewGuid() }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(schemaId.Id, command.SchemaId); + } + + [Fact] + public async Task Should_not_override_schema_id_and_name() + { + var command = new CreateContent { SchemaId = NamedId.Of(Guid.NewGuid(), "other-schema") }; + var context = Ctx(command); + + await sut.HandleAsync(context); + + Assert.NotEqual(appId, command.AppId); + } + + private CommandContext Ctx(ICommand command) + { + return new CommandContext(command, commandBus); + } + } +} diff --git a/tests/Squidex.Web.Tests/ExposedValuesTests.cs b/backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/ExposedValuesTests.cs rename to backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs new file mode 100644 index 000000000..75781d833 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// 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.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.UsageTracking; +using Xunit; + +namespace Squidex.Web.Pipeline +{ + public class ApiCostsFilterTests + { + private readonly IActionContextAccessor actionContextAccessor = A.Fake(); + private readonly IAppEntity appEntity = A.Fake(); + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IUsageTracker usageTracker = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly ActionExecutingContext actionContext; + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutionDelegate next; + private readonly ApiCostsFilter sut; + private long apiCallsMax; + private long apiCallsCurrent; + private bool isNextCalled; + + public ApiCostsFilterTests() + { + actionContext = + new ActionExecutingContext( + new ActionContext(httpContext, new RouteData(), + new ActionDescriptor()), + new List(), new Dictionary(), null); + + A.CallTo(() => actionContextAccessor.ActionContext) + .Returns(actionContext); + + A.CallTo(() => appPlansProvider.GetPlan(null)) + .Returns(appPlan); + + A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity)) + .Returns(appPlan); + + A.CallTo(() => appPlan.MaxApiCalls) + .ReturnsLazily(x => apiCallsMax); + + A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A.Ignored, DateTime.Today)) + .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + + sut = new ApiCostsFilter(appPlansProvider, usageTracker); + } + + [Fact] + public async Task Should_return_429_status_code_if_max_calls_over_limit() + { + sut.FilterDefinition = new ApiCostsAttribute(1); + + SetupApp(); + + apiCallsCurrent = 1000; + apiCallsMax = 600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.Equal(429, (actionContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_track_if_calls_left() + { + sut.FilterDefinition = new ApiCostsAttribute(13); + + SetupApp(); + + apiCallsCurrent = 1000; + apiCallsMax = 1600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_allow_small_buffer() + { + sut.FilterDefinition = new ApiCostsAttribute(13); + + SetupApp(); + + apiCallsCurrent = 1099; + apiCallsMax = 1000; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_track_if_weight_is_zero() + { + sut.FilterDefinition = new ApiCostsAttribute(0); + + SetupApp(); + + apiCallsCurrent = 1000; + apiCallsMax = 600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_track_if_app_not_defined() + { + sut.FilterDefinition = new ApiCostsAttribute(1); + + apiCallsCurrent = 1000; + apiCallsMax = 600; + + await sut.OnActionExecutionAsync(actionContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private void SetupApp() + { + httpContext.Context().App = appEntity; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs rename to backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs new file mode 100644 index 000000000..274e21d50 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Web.Pipeline +{ + public class AppResolverTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private readonly string appName = "my-app"; + private readonly AppResolver sut; + private bool isNextCalled; + + public AppResolverTests() + { + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + EndpointMetadata = new List() + }); + + actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + actionExecutingContext.RouteData.Values["app"] = appName; + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + + sut = new AppResolver(appProvider); + } + + [Fact] + public async Task Should_return_not_found_if_app_not_found() + { + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(Task.FromResult(null)); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_user() + { + var app = CreateApp(appName, appUser: "user1"); + + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_client() + { + var app = CreateApp(appName, appClient: "client1"); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_if_anonymous_but_not_permissions() + { + var app = CreateApp(appName); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.Equal(2, user.Claims.Count()); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_return_not_found_if_user_has_no_permissions() + { + var app = CreateApp(appName); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + actionExecutingContext.RouteData.Values.Remove("app"); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => appProvider.GetAppAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null) + { + var appEntity = A.Fake(); + + if (appUser != null) + { + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty.Assign(appUser, Role.Owner)); + } + else + { + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty); + } + + if (appClient != null) + { + A.CallTo(() => appEntity.Clients) + .Returns(AppClients.Empty.Add(appClient, "secret")); + } + else + { + A.CallTo(() => appEntity.Clients) + .Returns(AppClients.Empty); + } + + A.CallTo(() => appEntity.Name) + .Returns(name); + + A.CallTo(() => appEntity.Roles) + .Returns(Roles.Empty); + + return appEntity; + } + } +} diff --git a/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs rename to backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs new file mode 100644 index 000000000..b8080f243 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Squidex.Web.Pipeline +{ + public class ETagFilterTests + { + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutingContext executingContext; + private readonly ActionExecutedContext executedContext; + private readonly ETagFilter sut = new ETagFilter(Options.Create(new ETagOptions())); + + public ETagFilterTests() + { + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var filters = new List(); + + executingContext = new ActionExecutingContext(actionContext, filters, new Dictionary(), this); + executedContext = new ActionExecutedContext(actionContext, filters, this) + { + Result = new OkResult() + }; + } + + [Fact] + public async Task Should_not_convert_already_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_convert_strong_to_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_not_convert_empty_string_to_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_return_304_for_same_etags() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; + + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(304, (executedContext.Result as StatusCodeResult)!.StatusCode); + } + + [Fact] + public async Task Should_not_return_304_for_different_etags() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11"; + + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + + await sut.OnActionExecutionAsync(executingContext, Next()); + + Assert.Equal(200, (executedContext.Result as StatusCodeResult)!.StatusCode); + } + + private ActionExecutionDelegate Next() + { + return () => Task.FromResult(executedContext); + } + } +} diff --git a/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs similarity index 100% rename from tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs rename to backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj new file mode 100644 index 000000000..eb724ae8c --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -0,0 +1,33 @@ + + + Exe + netcoreapp3.0 + Squidex.Web + 8.0 + enable + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tests/docker-compose.yml b/backend/tests/docker-compose.yml similarity index 100% rename from tests/docker-compose.yml rename to backend/tests/docker-compose.yml diff --git a/backend/tools/GenerateLanguages/GenerateLanguages.csproj b/backend/tools/GenerateLanguages/GenerateLanguages.csproj new file mode 100644 index 000000000..762a53b43 --- /dev/null +++ b/backend/tools/GenerateLanguages/GenerateLanguages.csproj @@ -0,0 +1,16 @@ + + + netcoreapp3.0 + Exe + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/GenerateLanguages/GenerateLanguages.sln b/backend/tools/GenerateLanguages/GenerateLanguages.sln similarity index 100% rename from tools/GenerateLanguages/GenerateLanguages.sln rename to backend/tools/GenerateLanguages/GenerateLanguages.sln diff --git a/tools/GenerateLanguages/Program.cs b/backend/tools/GenerateLanguages/Program.cs similarity index 100% rename from tools/GenerateLanguages/Program.cs rename to backend/tools/GenerateLanguages/Program.cs diff --git a/backend/tools/LoadTest/LoadTest.csproj b/backend/tools/LoadTest/LoadTest.csproj new file mode 100644 index 000000000..6f04598b2 --- /dev/null +++ b/backend/tools/LoadTest/LoadTest.csproj @@ -0,0 +1,23 @@ + + + Exe + netcoreapp3.0 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/LoadTest/LoadTest.sln b/backend/tools/LoadTest/LoadTest.sln similarity index 100% rename from tools/LoadTest/LoadTest.sln rename to backend/tools/LoadTest/LoadTest.sln diff --git a/tools/LoadTest/Model/TestClient.cs b/backend/tools/LoadTest/Model/TestClient.cs similarity index 100% rename from tools/LoadTest/Model/TestClient.cs rename to backend/tools/LoadTest/Model/TestClient.cs diff --git a/tools/LoadTest/Model/TestEntity.cs b/backend/tools/LoadTest/Model/TestEntity.cs similarity index 100% rename from tools/LoadTest/Model/TestEntity.cs rename to backend/tools/LoadTest/Model/TestEntity.cs diff --git a/tools/LoadTest/ReadingBenchmarks.cs b/backend/tools/LoadTest/ReadingBenchmarks.cs similarity index 100% rename from tools/LoadTest/ReadingBenchmarks.cs rename to backend/tools/LoadTest/ReadingBenchmarks.cs diff --git a/tools/LoadTest/ReadingFixture.cs b/backend/tools/LoadTest/ReadingFixture.cs similarity index 100% rename from tools/LoadTest/ReadingFixture.cs rename to backend/tools/LoadTest/ReadingFixture.cs diff --git a/tools/LoadTest/Run.cs b/backend/tools/LoadTest/Run.cs similarity index 100% rename from tools/LoadTest/Run.cs rename to backend/tools/LoadTest/Run.cs diff --git a/tools/LoadTest/TestUtils.cs b/backend/tools/LoadTest/TestUtils.cs similarity index 100% rename from tools/LoadTest/TestUtils.cs rename to backend/tools/LoadTest/TestUtils.cs diff --git a/tools/LoadTest/Utils/Run.cs b/backend/tools/LoadTest/Utils/Run.cs similarity index 100% rename from tools/LoadTest/Utils/Run.cs rename to backend/tools/LoadTest/Utils/Run.cs diff --git a/tools/LoadTest/WritingBenchmarks.cs b/backend/tools/LoadTest/WritingBenchmarks.cs similarity index 100% rename from tools/LoadTest/WritingBenchmarks.cs rename to backend/tools/LoadTest/WritingBenchmarks.cs diff --git a/tools/LoadTest/WritingFixture.cs b/backend/tools/LoadTest/WritingFixture.cs similarity index 100% rename from tools/LoadTest/WritingFixture.cs rename to backend/tools/LoadTest/WritingFixture.cs diff --git a/backend/tools/Migrate_00/Migrate_00.csproj b/backend/tools/Migrate_00/Migrate_00.csproj new file mode 100644 index 000000000..bd4b2fa92 --- /dev/null +++ b/backend/tools/Migrate_00/Migrate_00.csproj @@ -0,0 +1,19 @@ + + + Exe + netcoreapp3.0 + 8.0 + enable + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/tools/Migrate_00/Program.cs b/backend/tools/Migrate_00/Program.cs similarity index 100% rename from tools/Migrate_00/Program.cs rename to backend/tools/Migrate_00/Program.cs diff --git a/backend/tools/Migrate_01/Migrate_01.csproj b/backend/tools/Migrate_01/Migrate_01.csproj new file mode 100644 index 000000000..c5d8b6620 --- /dev/null +++ b/backend/tools/Migrate_01/Migrate_01.csproj @@ -0,0 +1,25 @@ + + + netcoreapp3.0 + 8.0 + enable + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/backend/tools/Migrate_01/MigrationPath.cs b/backend/tools/Migrate_01/MigrationPath.cs new file mode 100644 index 000000000..6701d39ea --- /dev/null +++ b/backend/tools/Migrate_01/MigrationPath.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Migrate_01.Migrations; +using Migrate_01.Migrations.MongoDb; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01 +{ + public sealed class MigrationPath : IMigrationPath + { + private const int CurrentVersion = 19; + private readonly IServiceProvider serviceProvider; + + public MigrationPath(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public (int Version, IEnumerable? Migrations) GetNext(int version) + { + if (version == CurrentVersion) + { + return (CurrentVersion, null); + } + + var migrations = ResolveMigrators(version).Where(x => x != null).ToList(); + + return (CurrentVersion, migrations); + } + + private IEnumerable ResolveMigrators(int version) + { + yield return serviceProvider.GetRequiredService(); + + // Version 06: Convert Event store. Must always be executed first. + if (version < 6) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 07: Introduces AppId for backups. + else if (version < 7) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots. + if (version < 5) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 12: Introduce roles. + else if (version < 12) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 09: Grain indexes. + if (version < 9) + { + yield return serviceProvider.GetService(); + } + + // Version 19: Unify indexes. + if (version < 19) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 11: Introduce content drafts. + if (version < 11) + { + yield return serviceProvider.GetService(); + yield return serviceProvider.GetRequiredService(); + } + + // Version 13: Json refactoring + if (version < 13) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 14: Schema refactoring + if (version < 14) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 01: Introduce app patterns. + if (version < 1) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 15: Introduce custom full text search actors. + if (version < 15) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 17: Rename slug field. + if (version < 17) + { + yield return serviceProvider.GetService(); + } + + // Version 18: Rebuild assets. + if (version < 18) + { + yield return serviceProvider.GetService(); + } + + // Version 16: Introduce file name slugs for assets. + if (version < 16) + { + yield return serviceProvider.GetRequiredService(); + } + + yield return serviceProvider.GetRequiredService(); + } + } +} diff --git a/backend/tools/Migrate_01/Migrations/AddPatterns.cs b/backend/tools/Migrate_01/Migrations/AddPatterns.cs new file mode 100644 index 000000000..f3f087a9c --- /dev/null +++ b/backend/tools/Migrate_01/Migrations/AddPatterns.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class AddPatterns : IMigration + { + private readonly InitialPatterns initialPatterns; + private readonly ICommandBus commandBus; + private readonly IAppsIndex indexForApps; + + public AddPatterns(InitialPatterns initialPatterns, ICommandBus commandBus, IAppsIndex indexForApps) + { + this.indexForApps = indexForApps; + this.initialPatterns = initialPatterns; + this.commandBus = commandBus; + } + + public async Task UpdateAsync() + { + var ids = await indexForApps.GetIdsAsync(); + + foreach (var id in ids) + { + var app = await indexForApps.GetAppAsync(id); + + if (app != null && app.Patterns.Count == 0) + { + foreach (var pattern in initialPatterns.Values) + { + var command = + new AddPattern + { + Actor = app.CreatedBy, + AppId = id, + Name = pattern.Name, + PatternId = Guid.NewGuid(), + Pattern = pattern.Pattern, + Message = pattern.Message + }; + + await commandBus.PublishAsync(command); + } + } + } + } + } +} \ No newline at end of file diff --git a/tools/Migrate_01/Migrations/ClearSchemas.cs b/backend/tools/Migrate_01/Migrations/ClearSchemas.cs similarity index 100% rename from tools/Migrate_01/Migrations/ClearSchemas.cs rename to backend/tools/Migrate_01/Migrations/ClearSchemas.cs diff --git a/backend/tools/Migrate_01/Migrations/ConvertEventStore.cs b/backend/tools/Migrate_01/Migrations/ConvertEventStore.cs new file mode 100644 index 000000000..990431f37 --- /dev/null +++ b/backend/tools/Migrate_01/Migrations/ConvertEventStore.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertEventStore : IMigration + { + private readonly IEventStore eventStore; + + public ConvertEventStore(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public async Task UpdateAsync() + { + if (eventStore is MongoEventStore mongoEventStore) + { + var collection = mongoEventStore.RawCollection; + + var filter = Builders.Filter; + + var writesBatches = new List>(); + + async Task WriteAsync(WriteModel? model, bool force) + { + if (model != null) + { + writesBatches.Add(model); + } + + if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) + { + await collection.BulkWriteAsync(writesBatches); + + writesBatches.Clear(); + } + } + + await collection.Find(new BsonDocument()).ForEachAsync(async commit => + { + foreach (BsonDocument @event in commit["Events"].AsBsonArray) + { + var meta = JObject.Parse(@event["Metadata"].AsString); + + @event.Remove("EventId"); + @event["Metadata"] = meta.ToBson(); + } + + await WriteAsync(new ReplaceOneModel(filter.Eq("_id", commit["_id"].AsString), commit), false); + }); + + await WriteAsync(null, true); + } + } + } +} diff --git a/backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs b/backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs new file mode 100644 index 000000000..3a0271125 --- /dev/null +++ b/backend/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertEventStoreAppId : IMigration + { + private readonly IEventStore eventStore; + + public ConvertEventStoreAppId(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public async Task UpdateAsync() + { + if (eventStore is MongoEventStore mongoEventStore) + { + var collection = mongoEventStore.RawCollection; + + var filterer = Builders.Filter; + var updater = Builders.Update; + + var writesBatches = new List>(); + + async Task WriteAsync(WriteModel? model, bool force) + { + if (model != null) + { + writesBatches.Add(model); + } + + if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) + { + await collection.BulkWriteAsync(writesBatches); + + writesBatches.Clear(); + } + } + + await collection.Find(new BsonDocument()).ForEachAsync(async commit => + { + UpdateDefinition? update = null; + + var index = 0; + + foreach (BsonDocument @event in commit["Events"].AsBsonArray) + { + var data = JObject.Parse(@event["Payload"].AsString); + + if (data.TryGetValue("appId", out var appIdValue)) + { + var appId = NamedId.Parse(appIdValue.ToString(), Guid.TryParse).Id.ToString(); + + var eventUpdate = updater.Set($"Events.{index}.Metadata.{SquidexHeaders.AppId}", appId); + + if (update != null) + { + update = updater.Combine(update, eventUpdate); + } + else + { + update = eventUpdate; + } + } + + index++; + } + + if (update != null) + { + var write = new UpdateOneModel(filterer.Eq("_id", commit["_id"].AsString), update); + + await WriteAsync(write, false); + } + }); + + await WriteAsync(null, true); + } + } + } +} diff --git a/tools/Migrate_01/Migrations/CreateAssetSlugs.cs b/backend/tools/Migrate_01/Migrations/CreateAssetSlugs.cs similarity index 100% rename from tools/Migrate_01/Migrations/CreateAssetSlugs.cs rename to backend/tools/Migrate_01/Migrations/CreateAssetSlugs.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs b/backend/tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/ConvertOldSnapshotStores.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs b/backend/tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/ConvertRuleEventsJson.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs b/backend/tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/DeleteContentCollections.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs b/backend/tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/RenameAssetSlugField.cs diff --git a/tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs b/backend/tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs similarity index 100% rename from tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs rename to backend/tools/Migrate_01/Migrations/MongoDb/RestructureContentCollection.cs diff --git a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs b/backend/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs similarity index 100% rename from tools/Migrate_01/Migrations/PopulateGrainIndexes.cs rename to backend/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs diff --git a/tools/Migrate_01/Migrations/RebuildApps.cs b/backend/tools/Migrate_01/Migrations/RebuildApps.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildApps.cs rename to backend/tools/Migrate_01/Migrations/RebuildApps.cs diff --git a/tools/Migrate_01/Migrations/RebuildAssets.cs b/backend/tools/Migrate_01/Migrations/RebuildAssets.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildAssets.cs rename to backend/tools/Migrate_01/Migrations/RebuildAssets.cs diff --git a/tools/Migrate_01/Migrations/RebuildContents.cs b/backend/tools/Migrate_01/Migrations/RebuildContents.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildContents.cs rename to backend/tools/Migrate_01/Migrations/RebuildContents.cs diff --git a/tools/Migrate_01/Migrations/RebuildSnapshots.cs b/backend/tools/Migrate_01/Migrations/RebuildSnapshots.cs similarity index 100% rename from tools/Migrate_01/Migrations/RebuildSnapshots.cs rename to backend/tools/Migrate_01/Migrations/RebuildSnapshots.cs diff --git a/tools/Migrate_01/Migrations/StartEventConsumers.cs b/backend/tools/Migrate_01/Migrations/StartEventConsumers.cs similarity index 100% rename from tools/Migrate_01/Migrations/StartEventConsumers.cs rename to backend/tools/Migrate_01/Migrations/StartEventConsumers.cs diff --git a/tools/Migrate_01/Migrations/StopEventConsumers.cs b/backend/tools/Migrate_01/Migrations/StopEventConsumers.cs similarity index 100% rename from tools/Migrate_01/Migrations/StopEventConsumers.cs rename to backend/tools/Migrate_01/Migrations/StopEventConsumers.cs diff --git a/tools/Migrate_01/OldEvents/AppClientChanged.cs b/backend/tools/Migrate_01/OldEvents/AppClientChanged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppClientChanged.cs rename to backend/tools/Migrate_01/OldEvents/AppClientChanged.cs diff --git a/tools/Migrate_01/OldEvents/AppClientPermission.cs b/backend/tools/Migrate_01/OldEvents/AppClientPermission.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppClientPermission.cs rename to backend/tools/Migrate_01/OldEvents/AppClientPermission.cs diff --git a/tools/Migrate_01/OldEvents/AppClientUpdated.cs b/backend/tools/Migrate_01/OldEvents/AppClientUpdated.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppClientUpdated.cs rename to backend/tools/Migrate_01/OldEvents/AppClientUpdated.cs diff --git a/tools/Migrate_01/OldEvents/AppContributorAssigned.cs b/backend/tools/Migrate_01/OldEvents/AppContributorAssigned.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppContributorAssigned.cs rename to backend/tools/Migrate_01/OldEvents/AppContributorAssigned.cs diff --git a/tools/Migrate_01/OldEvents/AppContributorPermission.cs b/backend/tools/Migrate_01/OldEvents/AppContributorPermission.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppContributorPermission.cs rename to backend/tools/Migrate_01/OldEvents/AppContributorPermission.cs diff --git a/tools/Migrate_01/OldEvents/AppPlanChanged.cs b/backend/tools/Migrate_01/OldEvents/AppPlanChanged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppPlanChanged.cs rename to backend/tools/Migrate_01/OldEvents/AppPlanChanged.cs diff --git a/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs b/backend/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs rename to backend/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs diff --git a/tools/Migrate_01/OldEvents/AssetRenamed.cs b/backend/tools/Migrate_01/OldEvents/AssetRenamed.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AssetRenamed.cs rename to backend/tools/Migrate_01/OldEvents/AssetRenamed.cs diff --git a/tools/Migrate_01/OldEvents/AssetTagged.cs b/backend/tools/Migrate_01/OldEvents/AssetTagged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/AssetTagged.cs rename to backend/tools/Migrate_01/OldEvents/AssetTagged.cs diff --git a/tools/Migrate_01/OldEvents/ContentArchived.cs b/backend/tools/Migrate_01/OldEvents/ContentArchived.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentArchived.cs rename to backend/tools/Migrate_01/OldEvents/ContentArchived.cs diff --git a/tools/Migrate_01/OldEvents/ContentCreated.cs b/backend/tools/Migrate_01/OldEvents/ContentCreated.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentCreated.cs rename to backend/tools/Migrate_01/OldEvents/ContentCreated.cs diff --git a/tools/Migrate_01/OldEvents/ContentPublished.cs b/backend/tools/Migrate_01/OldEvents/ContentPublished.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentPublished.cs rename to backend/tools/Migrate_01/OldEvents/ContentPublished.cs diff --git a/tools/Migrate_01/OldEvents/ContentRestored.cs b/backend/tools/Migrate_01/OldEvents/ContentRestored.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentRestored.cs rename to backend/tools/Migrate_01/OldEvents/ContentRestored.cs diff --git a/tools/Migrate_01/OldEvents/ContentStatusChanged.cs b/backend/tools/Migrate_01/OldEvents/ContentStatusChanged.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentStatusChanged.cs rename to backend/tools/Migrate_01/OldEvents/ContentStatusChanged.cs diff --git a/tools/Migrate_01/OldEvents/ContentUnpublished.cs b/backend/tools/Migrate_01/OldEvents/ContentUnpublished.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ContentUnpublished.cs rename to backend/tools/Migrate_01/OldEvents/ContentUnpublished.cs diff --git a/tools/Migrate_01/OldEvents/SchemaCreated.cs b/backend/tools/Migrate_01/OldEvents/SchemaCreated.cs similarity index 100% rename from tools/Migrate_01/OldEvents/SchemaCreated.cs rename to backend/tools/Migrate_01/OldEvents/SchemaCreated.cs diff --git a/tools/Migrate_01/OldEvents/ScriptsConfigured.cs b/backend/tools/Migrate_01/OldEvents/ScriptsConfigured.cs similarity index 100% rename from tools/Migrate_01/OldEvents/ScriptsConfigured.cs rename to backend/tools/Migrate_01/OldEvents/ScriptsConfigured.cs diff --git a/tools/Migrate_01/OldEvents/WebhookAdded.cs b/backend/tools/Migrate_01/OldEvents/WebhookAdded.cs similarity index 100% rename from tools/Migrate_01/OldEvents/WebhookAdded.cs rename to backend/tools/Migrate_01/OldEvents/WebhookAdded.cs diff --git a/tools/Migrate_01/OldEvents/WebhookDeleted.cs b/backend/tools/Migrate_01/OldEvents/WebhookDeleted.cs similarity index 100% rename from tools/Migrate_01/OldEvents/WebhookDeleted.cs rename to backend/tools/Migrate_01/OldEvents/WebhookDeleted.cs diff --git a/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs b/backend/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs similarity index 100% rename from tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs rename to backend/tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs diff --git a/tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs b/backend/tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs similarity index 100% rename from tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs rename to backend/tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs diff --git a/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs b/backend/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs similarity index 100% rename from tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs rename to backend/tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs diff --git a/tools/Migrate_01/RebuildOptions.cs b/backend/tools/Migrate_01/RebuildOptions.cs similarity index 100% rename from tools/Migrate_01/RebuildOptions.cs rename to backend/tools/Migrate_01/RebuildOptions.cs diff --git a/backend/tools/Migrate_01/RebuildRunner.cs b/backend/tools/Migrate_01/RebuildRunner.cs new file mode 100644 index 000000000..32dea1cbe --- /dev/null +++ b/backend/tools/Migrate_01/RebuildRunner.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Migrate_01.Migrations; +using Squidex.Infrastructure; + +namespace Migrate_01 +{ + public sealed class RebuildRunner + { + private readonly Rebuilder rebuilder; + private readonly PopulateGrainIndexes populateGrainIndexes; + private readonly RebuildOptions rebuildOptions; + + public RebuildRunner(Rebuilder rebuilder, IOptions rebuildOptions, PopulateGrainIndexes populateGrainIndexes) + { + Guard.NotNull(rebuilder); + Guard.NotNull(rebuildOptions); + Guard.NotNull(populateGrainIndexes); + + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + this.populateGrainIndexes = populateGrainIndexes; + } + + public async Task RunAsync(CancellationToken ct) + { + if (rebuildOptions.Apps) + { + await rebuilder.RebuildAppsAsync(ct); + } + + if (rebuildOptions.Schemas) + { + await rebuilder.RebuildSchemasAsync(ct); + } + + if (rebuildOptions.Rules) + { + await rebuilder.RebuildRulesAsync(ct); + } + + if (rebuildOptions.Assets) + { + await rebuilder.RebuildAssetsAsync(ct); + } + + if (rebuildOptions.Contents) + { + await rebuilder.RebuildContentAsync(ct); + } + + if (rebuildOptions.Indexes) + { + await populateGrainIndexes.UpdateAsync(); + } + } + } +} diff --git a/tools/Migrate_01/Rebuilder.cs b/backend/tools/Migrate_01/Rebuilder.cs similarity index 100% rename from tools/Migrate_01/Rebuilder.cs rename to backend/tools/Migrate_01/Rebuilder.cs diff --git a/tools/Migrate_01/SquidexMigrations.cs b/backend/tools/Migrate_01/SquidexMigrations.cs similarity index 100% rename from tools/Migrate_01/SquidexMigrations.cs rename to backend/tools/Migrate_01/SquidexMigrations.cs diff --git a/build.ps1 b/build.ps1 index 5e33380b8..c6b27a268 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,11 +1,11 @@ # Build the image -docker build . -t squidex-build-image -f dockerfile.build +docker build . -t squidex-build-image -f dockerfile # Open the image docker create --name squidex-build-container squidex-build-image # Copy the output to the host file system -docker cp squidex-build-container:/out ./publish +docker cp squidex-build-container:/app/ ./publish # Cleanup docker rm squidex-build-container \ No newline at end of file diff --git a/build.sh b/build.sh index 1bd6b7e23..c6b27a268 100644 --- a/build.sh +++ b/build.sh @@ -1,11 +1,11 @@ # Build the image -docker build . -t squidex-build-image -f Dockerfile.build +docker build . -t squidex-build-image -f dockerfile # Open the image docker create --name squidex-build-container squidex-build-image # Copy the output to the host file system -docker cp squidex-build-container:/out ./publish +docker cp squidex-build-container:/app/ ./publish # Cleanup docker rm squidex-build-container \ No newline at end of file diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs deleted file mode 100644 index 1e5b02263..000000000 --- a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ========================================================================== -// 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; - -#pragma warning disable IDE0059 // Value assigned to symbol is never used - -namespace Squidex.Extensions.Actions.Algolia -{ - public sealed class AlgoliaActionHandler : RuleActionHandler - { - private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients; - - public AlgoliaActionHandler(RuleEventFormatter formatter) - : base(formatter) - { - clients = new ClientPool<(string AppId, string ApiKey, string IndexName), Index>(key => - { - var client = new AlgoliaClient(key.AppId, key.ApiKey); - - return client.InitIndex(key.IndexName); - }); - } - - protected override (string Description, AlgoliaJob Data) CreateJob(EnrichedEvent @event, AlgoliaAction action) - { - if (@event is EnrichedContentEvent contentEvent) - { - var contentId = contentEvent.Id.ToString(); - - var ruleDescription = string.Empty; - var ruleJob = new AlgoliaJob - { - AppId = action.AppId, - ApiKey = action.ApiKey, - ContentId = contentId, - IndexName = Format(action.IndexName, @event) - }; - - if (contentEvent.Type == EnrichedContentEventType.Deleted || - contentEvent.Type == EnrichedContentEventType.Unpublished) - { - ruleDescription = $"Delete entry from Algolia index: {action.IndexName}"; - } - else - { - ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; - - 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 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; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs deleted file mode 100644 index 9781e4ffe..000000000 --- a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// 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 - { - private const string Description = "Purge key in fastly"; - - private readonly IHttpClientFactory httpClientFactory; - - public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - - this.httpClientFactory = httpClientFactory; - } - - protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) - { - var 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 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; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs deleted file mode 100644 index 0fbd3cdbf..000000000 --- a/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// 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 - { - private const string Description = "Send message to slack"; - - private readonly IHttpClientFactory httpClientFactory; - - public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - - this.httpClientFactory = httpClientFactory; - } - - protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action) - { - var body = new { text = Format(action.Text, @event) }; - - var ruleJob = new SlackJob - { - RequestUrl = action.WebhookUrl.ToString(), - RequestBody = ToJson(body) - }; - - return (Description, ruleJob); - } - - protected override async Task 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; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs deleted file mode 100644 index 5ba6473b7..000000000 --- a/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// 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 - { - private const string Description = "Send a tweet"; - - private readonly TwitterOptions twitterOptions; - - public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) - : base(formatter) - { - Guard.NotNull(twitterOptions, nameof(twitterOptions)); - - this.twitterOptions = twitterOptions.Value; - } - - protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) - { - var ruleJob = new TweetJob - { - Text = Format(action.Text, @event), - AccessToken = action.AccessToken, - AccessSecret = action.AccessSecret - }; - - return (Description, ruleJob); - } - - protected override async Task ExecuteJobAsync(TweetJob job, CancellationToken ct = default) - { - var tokens = Tokens.Create( - twitterOptions.ClientId, - twitterOptions.ClientSecret, - job.AccessToken, - job.AccessSecret); - - var request = new Dictionary - { - ["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; } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs deleted file mode 100644 index d5e55991d..000000000 --- a/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// 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 - { - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(2); - private readonly IHttpClientFactory httpClientFactory; - - public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - - this.httpClientFactory = httpClientFactory; - } - - protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action) - { - 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 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; } - } -} diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj deleted file mode 100644 index 257aeaa39..000000000 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - netstandard2.0 - 7.3 - - - - - - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex/.sass-lint.yml b/frontend/.sass-lint.yml similarity index 100% rename from src/Squidex/.sass-lint.yml rename to frontend/.sass-lint.yml diff --git a/src/Squidex/app-config/karma-test-shim.js b/frontend/app-config/karma-test-shim.js similarity index 100% rename from src/Squidex/app-config/karma-test-shim.js rename to frontend/app-config/karma-test-shim.js diff --git a/src/Squidex/app-config/karma.conf.js b/frontend/app-config/karma.conf.js similarity index 100% rename from src/Squidex/app-config/karma.conf.js rename to frontend/app-config/karma.conf.js diff --git a/src/Squidex/app-config/karma.coverage.conf.js b/frontend/app-config/karma.coverage.conf.js similarity index 100% rename from src/Squidex/app-config/karma.coverage.conf.js rename to frontend/app-config/karma.coverage.conf.js diff --git a/frontend/app-config/webpack.config.js b/frontend/app-config/webpack.config.js new file mode 100644 index 000000000..f569ea05a --- /dev/null +++ b/frontend/app-config/webpack.config.js @@ -0,0 +1,376 @@ +const webpack = require('webpack'), + path = require('path'); + +const appRoot = path.resolve(__dirname, '..'); + +function root() { + var newArgs = Array.prototype.slice.call(arguments, 0); + + return path.join.apply(path, [appRoot].concat(newArgs)); +}; + +const plugins = { + // https://github.com/webpack-contrib/mini-css-extract-plugin + MiniCssExtractPlugin: require('mini-css-extract-plugin'), + // https://github.com/dividab/tsconfig-paths-webpack-plugin + TsconfigPathsPlugin: require('tsconfig-paths-webpack-plugin'), + // https://github.com/aackerman/circular-dependency-plugin + CircularDependencyPlugin: require('circular-dependency-plugin'), + // https://github.com/jantimon/html-webpack-plugin + HtmlWebpackPlugin: require('html-webpack-plugin'), + // https://webpack.js.org/plugins/terser-webpack-plugin/ + TerserPlugin: require('terser-webpack-plugin'), + // https://www.npmjs.com/package/@ngtools/webpack + NgToolsWebpack: require('@ngtools/webpack'), + // https://github.com/NMFR/optimize-css-assets-webpack-plugin + OptimizeCSSAssetsPlugin: require("optimize-css-assets-webpack-plugin"), + // https://github.com/jrparish/tslint-webpack-plugin + TsLintPlugin: require('tslint-webpack-plugin') +}; + +module.exports = function (env) { + const isDevServer = path.basename(require.main.filename) === 'webpack-dev-server.js'; + const isProduction = env && env.production; + const isTests = env && env.target === 'tests'; + const isCoverage = env && env.coverage; + const isAot = isProduction; + + const config = { + mode: isProduction ? 'production' : 'development', + + /** + * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack. + * + * See: https://webpack.js.org/configuration/devtool/ + */ + devtool: isProduction ? false : 'inline-source-map', + + /** + * Options affecting the resolving of modules. + * + * See: https://webpack.js.org/configuration/resolve/ + */ + resolve: { + /** + * An array of extensions that should be used to resolve modules. + * + * See: https://webpack.js.org/configuration/resolve/#resolve-extensions + */ + extensions: ['.ts', '.js', '.mjs', '.css', '.scss'], + modules: [ + root('app'), + root('app', 'theme'), + root('node_modules') + ], + + plugins: [ + new plugins.TsconfigPathsPlugin() + ] + }, + + /** + * Options affecting the normal modules. + * + * See: https://webpack.js.org/configuration/module/ + */ + module: { + /** + * An array of Rules which are matched to requests when modules are created. + * + * See: https://webpack.js.org/configuration/module/#module-rules + */ + rules: [{ + test: /\.mjs$/, + type: "javascript/auto", + include: [/node_modules/] + }, { + test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, + parser: { system: true }, + include: [/node_modules/] + }, { + test: /\.js\.flow$/, + use: [{ + loader: 'ignore-loader' + }], + include: [/node_modules/] + }, { + test: /\.map$/, + use: [{ + loader: 'ignore-loader' + }], + include: [/node_modules/] + }, { + test: /\.d\.ts$/, + use: [{ + loader: 'ignore-loader' + }], + include: [/node_modules/] + }, { + test: /\.(woff|woff2|ttf|eot)(\?.*$|$)/, + use: [{ + loader: 'file-loader?name=[name].[hash].[ext]', + options: { + outputPath: 'assets', + /* + * Use custom public path as ./ is not supported by fonts. + */ + publicPath: isDevServer ? undefined : 'assets' + } + }] + }, { + test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/, + use: [{ + loader: 'file-loader?name=[name].[hash].[ext]', + options: { + outputPath: 'assets' + } + }] + }, { + test: /\.css$/, + use: [ + plugins.MiniCssExtractPlugin.loader, + { + loader: 'css-loader' + }] + }, { + test: /\.scss$/, + use: [{ + loader: 'raw-loader' + }, { + loader: 'sass-loader', options: { + sassOptions: { + includePaths: [root('app', 'theme')] + } + } + }], + exclude: root('app', 'theme') + }] + }, + + plugins: [ + new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, root('./app'), {}), + new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/), + + /** + * Puts each bundle into a file and appends the hash of the file to the path. + * + * See: https://github.com/webpack-contrib/mini-css-extract-plugin + */ + new plugins.MiniCssExtractPlugin('[name].css'), + + new webpack.LoaderOptionsPlugin({ + options: { + htmlLoader: { + /** + * Define the root for images, so that we can use absolute urls. + * + * See: https://github.com/webpack/html-loader#Advanced_Options + */ + root: root('app', 'images') + }, + context: '/' + } + }), + + /** + * Detect circular dependencies in app. + * + * See: https://github.com/aackerman/circular-dependency-plugin + */ + new plugins.CircularDependencyPlugin({ + exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/, + // Add errors to webpack instead of warnings + failOnError: true + }), + ], + + devServer: { + headers: { + 'Access-Control-Allow-Origin': '*' + }, + historyApiFallback: true + } + }; + + if (!isTests) { + /** + * The entry point for the bundle. Our Angular app. + * + * See: https://webpack.js.org/configuration/entry-context/ + */ + config.entry = { + 'shims': './app/shims.ts', + 'app': './app/app.ts' + }; + + if (isProduction) { + config.output = { + /** + * The output directory as absolute path (required). + * + * See: https://webpack.js.org/configuration/output/#output-path + */ + path: root('/build/'), + + publicPath: './build/', + + /** + * Specifies the name of each output file on disk. + * + * See: https://webpack.js.org/configuration/output/#output-filename + */ + filename: '[name].js', + + /** + * The filename of non-entry chunks as relative path inside the output.path directory. + * + * See: https://webpack.js.org/configuration/output/#output-chunkfilename + */ + chunkFilename: '[id].[hash].chunk.js' + }; + } else { + config.output = { + filename: '[name].js', + + /** + * Set the public path, because we are running the website from another port (5000). + */ + publicPath: 'http://localhost:3000/' + }; + } + + config.plugins.push( + new plugins.HtmlWebpackPlugin({ + hash: true, + chunks: ['shims', 'app'], + chunksSortMode: 'manual', + template: 'app/index.html' + }) + ); + + config.plugins.push( + new plugins.HtmlWebpackPlugin({ + template: 'app/_theme.html', hash: true, chunksSortMode: 'none', filename: 'theme.html' + }) + ); + + config.plugins.push( + new plugins.TsLintPlugin({ + files: ['./app/**/*.ts'], + /** + * Path to a configuration file. + */ + config: root('tslint.json'), + /** + * Wait for linting and fail the build when linting error occur. + */ + waitForLinting: isProduction + }) + ); + } + + if (!isCoverage) { + config.plugins.push( + new plugins.NgToolsWebpack.AngularCompilerPlugin({ + directTemplateLoading: true, + entryModule: 'app/app.module#AppModule', + sourceMap: !isProduction, + skipCodeGeneration: !isAot, + tsConfigPath: './tsconfig.json' + }) + ); + } + + if (isProduction) { + config.optimization = { + minimizer: [ + new plugins.TerserPlugin({ + terserOptions: { + compress: true, + ecma: 5, + mangle: true, + output: { + comments: false + }, + safari10: true + }, + extractComments: true + }), + + new plugins.OptimizeCSSAssetsPlugin({}) + ] + }; + + config.performance = { + hints: false + }; + } + + if (isCoverage) { + // Do not instrument tests. + config.module.rules.push({ + test: /\.ts$/, + use: [{ + loader: 'ts-loader' + }], + include: [/\.(e2e|spec)\.ts$/], + }); + + // Use instrument loader for all normal files. + config.module.rules.push({ + test: /\.ts$/, + use: [{ + loader: 'istanbul-instrumenter-loader?esModules=true' + }, { + loader: 'ts-loader' + }], + exclude: [/\.(e2e|spec)\.ts$/] + }); + } else { + config.module.rules.push({ + test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + use: [{ + loader: plugins.NgToolsWebpack.NgToolsLoader + }] + }) + } + + if (isProduction) { + config.module.rules.push({ + test: /\.scss$/, + /* + * Extract the content from a bundle to a file. + * + * See: https://github.com/webpack-contrib/extract-text-webpack-plugin + */ + use: [ + plugins.MiniCssExtractPlugin.loader, + { + loader: 'css-loader' + }, { + loader: 'sass-loader' + }], + /* + * Do not include component styles. + */ + include: root('app', 'theme'), + }); + } else { + config.module.rules.push({ + test: /\.scss$/, + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader' + }, { + loader: 'sass-loader?sourceMap' + }], + /* + * Do not include component styles. + */ + include: root('app', 'theme') + }); + } + + return config; +}; \ No newline at end of file diff --git a/src/Squidex/wwwroot/_theme.html b/frontend/app/_theme.html similarity index 100% rename from src/Squidex/wwwroot/_theme.html rename to frontend/app/_theme.html diff --git a/src/Squidex/app/app.component.html b/frontend/app/app.component.html similarity index 100% rename from src/Squidex/app/app.component.html rename to frontend/app/app.component.html diff --git a/src/Squidex/app/app.component.scss b/frontend/app/app.component.scss similarity index 100% rename from src/Squidex/app/app.component.scss rename to frontend/app/app.component.scss diff --git a/src/Squidex/app/app.component.ts b/frontend/app/app.component.ts similarity index 100% rename from src/Squidex/app/app.component.ts rename to frontend/app/app.component.ts diff --git a/src/Squidex/app/app.module.ts b/frontend/app/app.module.ts similarity index 100% rename from src/Squidex/app/app.module.ts rename to frontend/app/app.module.ts diff --git a/src/Squidex/app/app.routes.ts b/frontend/app/app.routes.ts similarity index 100% rename from src/Squidex/app/app.routes.ts rename to frontend/app/app.routes.ts diff --git a/src/Squidex/app/app.ts b/frontend/app/app.ts similarity index 100% rename from src/Squidex/app/app.ts rename to frontend/app/app.ts diff --git a/src/Squidex/app/declarations.d.ts b/frontend/app/declarations.d.ts similarity index 100% rename from src/Squidex/app/declarations.d.ts rename to frontend/app/declarations.d.ts diff --git a/src/Squidex/app/features/administration/administration-area.component.html b/frontend/app/features/administration/administration-area.component.html similarity index 100% rename from src/Squidex/app/features/administration/administration-area.component.html rename to frontend/app/features/administration/administration-area.component.html diff --git a/src/Squidex/app/features/administration/administration-area.component.scss b/frontend/app/features/administration/administration-area.component.scss similarity index 100% rename from src/Squidex/app/features/administration/administration-area.component.scss rename to frontend/app/features/administration/administration-area.component.scss diff --git a/src/Squidex/app/features/administration/administration-area.component.ts b/frontend/app/features/administration/administration-area.component.ts similarity index 100% rename from src/Squidex/app/features/administration/administration-area.component.ts rename to frontend/app/features/administration/administration-area.component.ts diff --git a/src/Squidex/app/features/administration/declarations.ts b/frontend/app/features/administration/declarations.ts similarity index 100% rename from src/Squidex/app/features/administration/declarations.ts rename to frontend/app/features/administration/declarations.ts diff --git a/src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts b/frontend/app/features/administration/guards/unset-user.guard.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts rename to frontend/app/features/administration/guards/unset-user.guard.spec.ts diff --git a/src/Squidex/app/features/administration/guards/unset-user.guard.ts b/frontend/app/features/administration/guards/unset-user.guard.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/unset-user.guard.ts rename to frontend/app/features/administration/guards/unset-user.guard.ts diff --git a/src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts b/frontend/app/features/administration/guards/user-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts rename to frontend/app/features/administration/guards/user-must-exist.guard.spec.ts diff --git a/src/Squidex/app/features/administration/guards/user-must-exist.guard.ts b/frontend/app/features/administration/guards/user-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/features/administration/guards/user-must-exist.guard.ts rename to frontend/app/features/administration/guards/user-must-exist.guard.ts diff --git a/src/Squidex/app/features/administration/internal.ts b/frontend/app/features/administration/internal.ts similarity index 100% rename from src/Squidex/app/features/administration/internal.ts rename to frontend/app/features/administration/internal.ts diff --git a/src/Squidex/app/features/administration/module.ts b/frontend/app/features/administration/module.ts similarity index 100% rename from src/Squidex/app/features/administration/module.ts rename to frontend/app/features/administration/module.ts diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumer.component.ts b/frontend/app/features/administration/pages/event-consumers/event-consumer.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumer.component.ts rename to frontend/app/features/administration/pages/event-consumers/event-consumer.component.ts diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html rename to frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss rename to frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts rename to frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.html b/frontend/app/features/administration/pages/restore/restore-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/restore/restore-page.component.html rename to frontend/app/features/administration/pages/restore/restore-page.component.html diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.scss b/frontend/app/features/administration/pages/restore/restore-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/restore/restore-page.component.scss rename to frontend/app/features/administration/pages/restore/restore-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts b/frontend/app/features/administration/pages/restore/restore-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/restore/restore-page.component.ts rename to frontend/app/features/administration/pages/restore/restore-page.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.html b/frontend/app/features/administration/pages/users/user-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user-page.component.html rename to frontend/app/features/administration/pages/users/user-page.component.html diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.scss b/frontend/app/features/administration/pages/users/user-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user-page.component.scss rename to frontend/app/features/administration/pages/users/user-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/frontend/app/features/administration/pages/users/user-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user-page.component.ts rename to frontend/app/features/administration/pages/users/user-page.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/user.component.ts b/frontend/app/features/administration/pages/users/user.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/users/user.component.ts rename to frontend/app/features/administration/pages/users/user.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/frontend/app/features/administration/pages/users/users-page.component.html similarity index 100% rename from src/Squidex/app/features/administration/pages/users/users-page.component.html rename to frontend/app/features/administration/pages/users/users-page.component.html diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.scss b/frontend/app/features/administration/pages/users/users-page.component.scss similarity index 100% rename from src/Squidex/app/features/administration/pages/users/users-page.component.scss rename to frontend/app/features/administration/pages/users/users-page.component.scss diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.ts b/frontend/app/features/administration/pages/users/users-page.component.ts similarity index 100% rename from src/Squidex/app/features/administration/pages/users/users-page.component.ts rename to frontend/app/features/administration/pages/users/users-page.component.ts diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts b/frontend/app/features/administration/services/event-consumers.service.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/services/event-consumers.service.spec.ts rename to frontend/app/features/administration/services/event-consumers.service.spec.ts diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.ts b/frontend/app/features/administration/services/event-consumers.service.ts similarity index 100% rename from src/Squidex/app/features/administration/services/event-consumers.service.ts rename to frontend/app/features/administration/services/event-consumers.service.ts diff --git a/src/Squidex/app/features/administration/services/users.service.spec.ts b/frontend/app/features/administration/services/users.service.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/services/users.service.spec.ts rename to frontend/app/features/administration/services/users.service.spec.ts diff --git a/src/Squidex/app/features/administration/services/users.service.ts b/frontend/app/features/administration/services/users.service.ts similarity index 100% rename from src/Squidex/app/features/administration/services/users.service.ts rename to frontend/app/features/administration/services/users.service.ts diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/frontend/app/features/administration/state/event-consumers.state.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/state/event-consumers.state.spec.ts rename to frontend/app/features/administration/state/event-consumers.state.spec.ts diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/frontend/app/features/administration/state/event-consumers.state.ts similarity index 100% rename from src/Squidex/app/features/administration/state/event-consumers.state.ts rename to frontend/app/features/administration/state/event-consumers.state.ts diff --git a/src/Squidex/app/features/administration/state/users.forms.ts b/frontend/app/features/administration/state/users.forms.ts similarity index 100% rename from src/Squidex/app/features/administration/state/users.forms.ts rename to frontend/app/features/administration/state/users.forms.ts diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/frontend/app/features/administration/state/users.state.spec.ts similarity index 100% rename from src/Squidex/app/features/administration/state/users.state.spec.ts rename to frontend/app/features/administration/state/users.state.spec.ts diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/frontend/app/features/administration/state/users.state.ts similarity index 100% rename from src/Squidex/app/features/administration/state/users.state.ts rename to frontend/app/features/administration/state/users.state.ts diff --git a/src/Squidex/app/features/api/api-area.component.html b/frontend/app/features/api/api-area.component.html similarity index 100% rename from src/Squidex/app/features/api/api-area.component.html rename to frontend/app/features/api/api-area.component.html diff --git a/src/Squidex/app/features/api/api-area.component.scss b/frontend/app/features/api/api-area.component.scss similarity index 100% rename from src/Squidex/app/features/api/api-area.component.scss rename to frontend/app/features/api/api-area.component.scss diff --git a/src/Squidex/app/features/api/api-area.component.ts b/frontend/app/features/api/api-area.component.ts similarity index 100% rename from src/Squidex/app/features/api/api-area.component.ts rename to frontend/app/features/api/api-area.component.ts diff --git a/src/Squidex/app/features/api/declarations.ts b/frontend/app/features/api/declarations.ts similarity index 100% rename from src/Squidex/app/features/api/declarations.ts rename to frontend/app/features/api/declarations.ts diff --git a/src/Squidex/app/features/api/index.ts b/frontend/app/features/api/index.ts similarity index 100% rename from src/Squidex/app/features/api/index.ts rename to frontend/app/features/api/index.ts diff --git a/src/Squidex/app/features/api/module.ts b/frontend/app/features/api/module.ts similarity index 100% rename from src/Squidex/app/features/api/module.ts rename to frontend/app/features/api/module.ts diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html b/frontend/app/features/api/pages/graphql/graphql-page.component.html similarity index 100% rename from src/Squidex/app/features/api/pages/graphql/graphql-page.component.html rename to frontend/app/features/api/pages/graphql/graphql-page.component.html diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss b/frontend/app/features/api/pages/graphql/graphql-page.component.scss similarity index 100% rename from src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss rename to frontend/app/features/api/pages/graphql/graphql-page.component.scss diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts b/frontend/app/features/api/pages/graphql/graphql-page.component.ts similarity index 100% rename from src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts rename to frontend/app/features/api/pages/graphql/graphql-page.component.ts diff --git a/src/Squidex/app/features/apps/declarations.ts b/frontend/app/features/apps/declarations.ts similarity index 100% rename from src/Squidex/app/features/apps/declarations.ts rename to frontend/app/features/apps/declarations.ts diff --git a/src/Squidex/app/features/apps/index.ts b/frontend/app/features/apps/index.ts similarity index 100% rename from src/Squidex/app/features/apps/index.ts rename to frontend/app/features/apps/index.ts diff --git a/src/Squidex/app/features/apps/module.ts b/frontend/app/features/apps/module.ts similarity index 100% rename from src/Squidex/app/features/apps/module.ts rename to frontend/app/features/apps/module.ts diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/frontend/app/features/apps/pages/apps-page.component.html similarity index 100% rename from src/Squidex/app/features/apps/pages/apps-page.component.html rename to frontend/app/features/apps/pages/apps-page.component.html diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.scss b/frontend/app/features/apps/pages/apps-page.component.scss similarity index 100% rename from src/Squidex/app/features/apps/pages/apps-page.component.scss rename to frontend/app/features/apps/pages/apps-page.component.scss diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/frontend/app/features/apps/pages/apps-page.component.ts similarity index 100% rename from src/Squidex/app/features/apps/pages/apps-page.component.ts rename to frontend/app/features/apps/pages/apps-page.component.ts diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.html b/frontend/app/features/apps/pages/news-dialog.component.html similarity index 100% rename from src/Squidex/app/features/apps/pages/news-dialog.component.html rename to frontend/app/features/apps/pages/news-dialog.component.html diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.scss b/frontend/app/features/apps/pages/news-dialog.component.scss similarity index 100% rename from src/Squidex/app/features/apps/pages/news-dialog.component.scss rename to frontend/app/features/apps/pages/news-dialog.component.scss diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.ts b/frontend/app/features/apps/pages/news-dialog.component.ts similarity index 100% rename from src/Squidex/app/features/apps/pages/news-dialog.component.ts rename to frontend/app/features/apps/pages/news-dialog.component.ts diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html b/frontend/app/features/apps/pages/onboarding-dialog.component.html similarity index 100% rename from src/Squidex/app/features/apps/pages/onboarding-dialog.component.html rename to frontend/app/features/apps/pages/onboarding-dialog.component.html diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss b/frontend/app/features/apps/pages/onboarding-dialog.component.scss similarity index 100% rename from src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss rename to frontend/app/features/apps/pages/onboarding-dialog.component.scss diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts b/frontend/app/features/apps/pages/onboarding-dialog.component.ts similarity index 100% rename from src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts rename to frontend/app/features/apps/pages/onboarding-dialog.component.ts diff --git a/src/Squidex/app/features/assets/declarations.ts b/frontend/app/features/assets/declarations.ts similarity index 100% rename from src/Squidex/app/features/assets/declarations.ts rename to frontend/app/features/assets/declarations.ts diff --git a/src/Squidex/app/features/assets/index.ts b/frontend/app/features/assets/index.ts similarity index 100% rename from src/Squidex/app/features/assets/index.ts rename to frontend/app/features/assets/index.ts diff --git a/src/Squidex/app/features/assets/module.ts b/frontend/app/features/assets/module.ts similarity index 100% rename from src/Squidex/app/features/assets/module.ts rename to frontend/app/features/assets/module.ts diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html b/frontend/app/features/assets/pages/assets-filters-page.component.html similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-filters-page.component.html rename to frontend/app/features/assets/pages/assets-filters-page.component.html diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.scss b/frontend/app/features/assets/pages/assets-filters-page.component.scss similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-filters-page.component.scss rename to frontend/app/features/assets/pages/assets-filters-page.component.scss diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts b/frontend/app/features/assets/pages/assets-filters-page.component.ts similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-filters-page.component.ts rename to frontend/app/features/assets/pages/assets-filters-page.component.ts diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/frontend/app/features/assets/pages/assets-page.component.html similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-page.component.html rename to frontend/app/features/assets/pages/assets-page.component.html diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.scss b/frontend/app/features/assets/pages/assets-page.component.scss similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-page.component.scss rename to frontend/app/features/assets/pages/assets-page.component.scss diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/frontend/app/features/assets/pages/assets-page.component.ts similarity index 100% rename from src/Squidex/app/features/assets/pages/assets-page.component.ts rename to frontend/app/features/assets/pages/assets-page.component.ts diff --git a/src/Squidex/app/features/content/declarations.ts b/frontend/app/features/content/declarations.ts similarity index 100% rename from src/Squidex/app/features/content/declarations.ts rename to frontend/app/features/content/declarations.ts diff --git a/src/Squidex/app/features/content/index.ts b/frontend/app/features/content/index.ts similarity index 100% rename from src/Squidex/app/features/content/index.ts rename to frontend/app/features/content/index.ts diff --git a/src/Squidex/app/features/content/module.ts b/frontend/app/features/content/module.ts similarity index 100% rename from src/Squidex/app/features/content/module.ts rename to frontend/app/features/content/module.ts diff --git a/src/Squidex/app/features/content/pages/comments/comments-page.component.html b/frontend/app/features/content/pages/comments/comments-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/comments/comments-page.component.html rename to frontend/app/features/content/pages/comments/comments-page.component.html diff --git a/src/Squidex/app/features/content/pages/comments/comments-page.component.scss b/frontend/app/features/content/pages/comments/comments-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/comments/comments-page.component.scss rename to frontend/app/features/content/pages/comments/comments-page.component.scss diff --git a/src/Squidex/app/features/content/pages/comments/comments-page.component.ts b/frontend/app/features/content/pages/comments/comments-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/comments/comments-page.component.ts rename to frontend/app/features/content/pages/comments/comments-page.component.ts diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/frontend/app/features/content/pages/content/content-field.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-field.component.html rename to frontend/app/features/content/pages/content/content-field.component.html diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.scss b/frontend/app/features/content/pages/content/content-field.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-field.component.scss rename to frontend/app/features/content/pages/content/content-field.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/frontend/app/features/content/pages/content/content-field.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-field.component.ts rename to frontend/app/features/content/pages/content/content-field.component.ts diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.html b/frontend/app/features/content/pages/content/content-history-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-history-page.component.html rename to frontend/app/features/content/pages/content/content-history-page.component.html diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.scss b/frontend/app/features/content/pages/content/content-history-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-history-page.component.scss rename to frontend/app/features/content/pages/content/content-history-page.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.ts b/frontend/app/features/content/pages/content/content-history-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-history-page.component.ts rename to frontend/app/features/content/pages/content/content-history-page.component.ts diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-page.component.html rename to frontend/app/features/content/pages/content/content-page.component.html diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.scss b/frontend/app/features/content/pages/content/content-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-page.component.scss rename to frontend/app/features/content/pages/content/content-page.component.scss diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/frontend/app/features/content/pages/content/content-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/content-page.component.ts rename to frontend/app/features/content/pages/content/content-page.component.ts diff --git a/src/Squidex/app/features/content/pages/content/field-languages.component.ts b/frontend/app/features/content/pages/content/field-languages.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/content/field-languages.component.ts rename to frontend/app/features/content/pages/content/field-languages.component.ts diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html b/frontend/app/features/content/pages/contents/contents-filters-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html rename to frontend/app/features/content/pages/contents/contents-filters-page.component.html diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss b/frontend/app/features/content/pages/contents/contents-filters-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss rename to frontend/app/features/content/pages/contents/contents-filters-page.component.scss diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts b/frontend/app/features/content/pages/contents/contents-filters-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts rename to frontend/app/features/content/pages/contents/contents-filters-page.component.ts diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/frontend/app/features/content/pages/contents/contents-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-page.component.html rename to frontend/app/features/content/pages/contents/contents-page.component.html diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.scss b/frontend/app/features/content/pages/contents/contents-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-page.component.scss rename to frontend/app/features/content/pages/contents/contents-page.component.scss diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/contents/contents-page.component.ts rename to frontend/app/features/content/pages/contents/contents-page.component.ts diff --git a/src/Squidex/app/features/content/pages/messages.ts b/frontend/app/features/content/pages/messages.ts similarity index 100% rename from src/Squidex/app/features/content/pages/messages.ts rename to frontend/app/features/content/pages/messages.ts diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html b/frontend/app/features/content/pages/schemas/schemas-page.component.html similarity index 100% rename from src/Squidex/app/features/content/pages/schemas/schemas-page.component.html rename to frontend/app/features/content/pages/schemas/schemas-page.component.html diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss b/frontend/app/features/content/pages/schemas/schemas-page.component.scss similarity index 100% rename from src/Squidex/app/features/content/pages/schemas/schemas-page.component.scss rename to frontend/app/features/content/pages/schemas/schemas-page.component.scss diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/frontend/app/features/content/pages/schemas/schemas-page.component.ts similarity index 100% rename from src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts rename to frontend/app/features/content/pages/schemas/schemas-page.component.ts diff --git a/src/Squidex/app/features/content/shared/array-editor.component.html b/frontend/app/features/content/shared/array-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/array-editor.component.html rename to frontend/app/features/content/shared/array-editor.component.html diff --git a/src/Squidex/app/features/content/shared/array-editor.component.scss b/frontend/app/features/content/shared/array-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/array-editor.component.scss rename to frontend/app/features/content/shared/array-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/array-editor.component.ts b/frontend/app/features/content/shared/array-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/array-editor.component.ts rename to frontend/app/features/content/shared/array-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/array-item.component.html b/frontend/app/features/content/shared/array-item.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/array-item.component.html rename to frontend/app/features/content/shared/array-item.component.html diff --git a/src/Squidex/app/features/content/shared/array-item.component.scss b/frontend/app/features/content/shared/array-item.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/array-item.component.scss rename to frontend/app/features/content/shared/array-item.component.scss diff --git a/src/Squidex/app/features/content/shared/array-item.component.ts b/frontend/app/features/content/shared/array-item.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/array-item.component.ts rename to frontend/app/features/content/shared/array-item.component.ts diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.html b/frontend/app/features/content/shared/assets-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/assets-editor.component.html rename to frontend/app/features/content/shared/assets-editor.component.html diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.scss b/frontend/app/features/content/shared/assets-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/assets-editor.component.scss rename to frontend/app/features/content/shared/assets-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/frontend/app/features/content/shared/assets-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/assets-editor.component.ts rename to frontend/app/features/content/shared/assets-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/content-selector-item.component.ts b/frontend/app/features/content/shared/content-selector-item.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-selector-item.component.ts rename to frontend/app/features/content/shared/content-selector-item.component.ts diff --git a/src/Squidex/app/features/content/shared/content-status.component.html b/frontend/app/features/content/shared/content-status.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/content-status.component.html rename to frontend/app/features/content/shared/content-status.component.html diff --git a/src/Squidex/app/features/content/shared/content-status.component.scss b/frontend/app/features/content/shared/content-status.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/content-status.component.scss rename to frontend/app/features/content/shared/content-status.component.scss diff --git a/src/Squidex/app/features/content/shared/content-status.component.ts b/frontend/app/features/content/shared/content-status.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-status.component.ts rename to frontend/app/features/content/shared/content-status.component.ts diff --git a/src/Squidex/app/features/content/shared/content-value-editor.component.ts b/frontend/app/features/content/shared/content-value-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-value-editor.component.ts rename to frontend/app/features/content/shared/content-value-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/content-value.component.ts b/frontend/app/features/content/shared/content-value.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content-value.component.ts rename to frontend/app/features/content/shared/content-value.component.ts diff --git a/src/Squidex/app/features/content/shared/content.component.html b/frontend/app/features/content/shared/content.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/content.component.html rename to frontend/app/features/content/shared/content.component.html diff --git a/src/Squidex/app/features/content/shared/content.component.scss b/frontend/app/features/content/shared/content.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/content.component.scss rename to frontend/app/features/content/shared/content.component.scss diff --git a/src/Squidex/app/features/content/shared/content.component.ts b/frontend/app/features/content/shared/content.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/content.component.ts rename to frontend/app/features/content/shared/content.component.ts diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/frontend/app/features/content/shared/contents-selector.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/contents-selector.component.html rename to frontend/app/features/content/shared/contents-selector.component.html diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.scss b/frontend/app/features/content/shared/contents-selector.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/contents-selector.component.scss rename to frontend/app/features/content/shared/contents-selector.component.scss diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.ts b/frontend/app/features/content/shared/contents-selector.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/contents-selector.component.ts rename to frontend/app/features/content/shared/contents-selector.component.ts diff --git a/src/Squidex/app/features/content/shared/due-time-selector.component.html b/frontend/app/features/content/shared/due-time-selector.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/due-time-selector.component.html rename to frontend/app/features/content/shared/due-time-selector.component.html diff --git a/src/Squidex/app/features/content/shared/due-time-selector.component.scss b/frontend/app/features/content/shared/due-time-selector.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/due-time-selector.component.scss rename to frontend/app/features/content/shared/due-time-selector.component.scss diff --git a/src/Squidex/app/features/content/shared/due-time-selector.component.ts b/frontend/app/features/content/shared/due-time-selector.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/due-time-selector.component.ts rename to frontend/app/features/content/shared/due-time-selector.component.ts diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/frontend/app/features/content/shared/field-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/field-editor.component.html rename to frontend/app/features/content/shared/field-editor.component.html diff --git a/src/Squidex/app/features/content/shared/field-editor.component.scss b/frontend/app/features/content/shared/field-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/field-editor.component.scss rename to frontend/app/features/content/shared/field-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/field-editor.component.ts b/frontend/app/features/content/shared/field-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/field-editor.component.ts rename to frontend/app/features/content/shared/field-editor.component.ts diff --git a/src/Squidex/app/features/content/shared/preview-button.component.html b/frontend/app/features/content/shared/preview-button.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/preview-button.component.html rename to frontend/app/features/content/shared/preview-button.component.html diff --git a/src/Squidex/app/features/content/shared/preview-button.component.scss b/frontend/app/features/content/shared/preview-button.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/preview-button.component.scss rename to frontend/app/features/content/shared/preview-button.component.scss diff --git a/src/Squidex/app/features/content/shared/preview-button.component.ts b/frontend/app/features/content/shared/preview-button.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/preview-button.component.ts rename to frontend/app/features/content/shared/preview-button.component.ts diff --git a/src/Squidex/app/features/content/shared/reference-item.component.scss b/frontend/app/features/content/shared/reference-item.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/reference-item.component.scss rename to frontend/app/features/content/shared/reference-item.component.scss diff --git a/src/Squidex/app/features/content/shared/reference-item.component.ts b/frontend/app/features/content/shared/reference-item.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/reference-item.component.ts rename to frontend/app/features/content/shared/reference-item.component.ts diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/frontend/app/features/content/shared/references-editor.component.html similarity index 100% rename from src/Squidex/app/features/content/shared/references-editor.component.html rename to frontend/app/features/content/shared/references-editor.component.html diff --git a/src/Squidex/app/features/content/shared/references-editor.component.scss b/frontend/app/features/content/shared/references-editor.component.scss similarity index 100% rename from src/Squidex/app/features/content/shared/references-editor.component.scss rename to frontend/app/features/content/shared/references-editor.component.scss diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/frontend/app/features/content/shared/references-editor.component.ts similarity index 100% rename from src/Squidex/app/features/content/shared/references-editor.component.ts rename to frontend/app/features/content/shared/references-editor.component.ts diff --git a/src/Squidex/app/features/dashboard/declarations.ts b/frontend/app/features/dashboard/declarations.ts similarity index 100% rename from src/Squidex/app/features/dashboard/declarations.ts rename to frontend/app/features/dashboard/declarations.ts diff --git a/src/Squidex/app/features/dashboard/index.ts b/frontend/app/features/dashboard/index.ts similarity index 100% rename from src/Squidex/app/features/dashboard/index.ts rename to frontend/app/features/dashboard/index.ts diff --git a/src/Squidex/app/features/dashboard/module.ts b/frontend/app/features/dashboard/module.ts similarity index 100% rename from src/Squidex/app/features/dashboard/module.ts rename to frontend/app/features/dashboard/module.ts diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html b/frontend/app/features/dashboard/pages/dashboard-page.component.html similarity index 100% rename from src/Squidex/app/features/dashboard/pages/dashboard-page.component.html rename to frontend/app/features/dashboard/pages/dashboard-page.component.html diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss b/frontend/app/features/dashboard/pages/dashboard-page.component.scss similarity index 100% rename from src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss rename to frontend/app/features/dashboard/pages/dashboard-page.component.scss diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/frontend/app/features/dashboard/pages/dashboard-page.component.ts similarity index 100% rename from src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts rename to frontend/app/features/dashboard/pages/dashboard-page.component.ts diff --git a/src/Squidex/app/features/rules/declarations.ts b/frontend/app/features/rules/declarations.ts similarity index 100% rename from src/Squidex/app/features/rules/declarations.ts rename to frontend/app/features/rules/declarations.ts diff --git a/src/Squidex/app/features/rules/index.ts b/frontend/app/features/rules/index.ts similarity index 100% rename from src/Squidex/app/features/rules/index.ts rename to frontend/app/features/rules/index.ts diff --git a/src/Squidex/app/features/rules/module.ts b/frontend/app/features/rules/module.ts similarity index 100% rename from src/Squidex/app/features/rules/module.ts rename to frontend/app/features/rules/module.ts diff --git a/src/Squidex/app/features/rules/pages/events/pipes.ts b/frontend/app/features/rules/pages/events/pipes.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/events/pipes.ts rename to frontend/app/features/rules/pages/events/pipes.ts diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html b/frontend/app/features/rules/pages/events/rule-events-page.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/events/rule-events-page.component.html rename to frontend/app/features/rules/pages/events/rule-events-page.component.html diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss b/frontend/app/features/rules/pages/events/rule-events-page.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss rename to frontend/app/features/rules/pages/events/rule-events-page.component.scss diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts b/frontend/app/features/rules/pages/events/rule-events-page.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts rename to frontend/app/features/rules/pages/events/rule-events-page.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html rename to frontend/app/features/rules/pages/rules/actions/generic-action.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss b/frontend/app/features/rules/pages/rules/actions/generic-action.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss rename to frontend/app/features/rules/pages/rules/actions/generic-action.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts b/frontend/app/features/rules/pages/rules/actions/generic-action.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts rename to frontend/app/features/rules/pages/rules/actions/generic-action.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.html b/frontend/app/features/rules/pages/rules/rule-element.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-element.component.html rename to frontend/app/features/rules/pages/rules/rule-element.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.scss b/frontend/app/features/rules/pages/rules/rule-element.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-element.component.scss rename to frontend/app/features/rules/pages/rules/rule-element.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.ts b/frontend/app/features/rules/pages/rules/rule-element.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-element.component.ts rename to frontend/app/features/rules/pages/rules/rule-element.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule-icon.component.ts b/frontend/app/features/rules/pages/rules/rule-icon.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-icon.component.ts rename to frontend/app/features/rules/pages/rules/rule-icon.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/frontend/app/features/rules/pages/rules/rule-wizard.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html rename to frontend/app/features/rules/pages/rules/rule-wizard.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss b/frontend/app/features/rules/pages/rules/rule-wizard.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss rename to frontend/app/features/rules/pages/rules/rule-wizard.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/frontend/app/features/rules/pages/rules/rule-wizard.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts rename to frontend/app/features/rules/pages/rules/rule-wizard.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rule.component.html b/frontend/app/features/rules/pages/rules/rule.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule.component.html rename to frontend/app/features/rules/pages/rules/rule.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rule.component.scss b/frontend/app/features/rules/pages/rules/rule.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule.component.scss rename to frontend/app/features/rules/pages/rules/rule.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rule.component.ts b/frontend/app/features/rules/pages/rules/rule.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rule.component.ts rename to frontend/app/features/rules/pages/rules/rule.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/frontend/app/features/rules/pages/rules/rules-page.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rules-page.component.html rename to frontend/app/features/rules/pages/rules/rules-page.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss b/frontend/app/features/rules/pages/rules/rules-page.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rules-page.component.scss rename to frontend/app/features/rules/pages/rules/rules-page.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/frontend/app/features/rules/pages/rules/rules-page.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/rules-page.component.ts rename to frontend/app/features/rules/pages/rules/rules-page.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html rename to frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.scss similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss rename to frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.scss diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts similarity index 100% rename from src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts rename to frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts diff --git a/src/Squidex/app/features/schemas/declarations.ts b/frontend/app/features/schemas/declarations.ts similarity index 100% rename from src/Squidex/app/features/schemas/declarations.ts rename to frontend/app/features/schemas/declarations.ts diff --git a/src/Squidex/app/features/schemas/index.ts b/frontend/app/features/schemas/index.ts similarity index 100% rename from src/Squidex/app/features/schemas/index.ts rename to frontend/app/features/schemas/index.ts diff --git a/src/Squidex/app/features/schemas/module.ts b/frontend/app/features/schemas/module.ts similarity index 100% rename from src/Squidex/app/features/schemas/module.ts rename to frontend/app/features/schemas/module.ts diff --git a/src/Squidex/app/features/schemas/pages/messages.ts b/frontend/app/features/schemas/pages/messages.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/messages.ts rename to frontend/app/features/schemas/pages/messages.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/frontend/app/features/schemas/pages/schema/field-wizard.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html rename to frontend/app/features/schemas/pages/schema/field-wizard.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss b/frontend/app/features/schemas/pages/schema/field-wizard.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss rename to frontend/app/features/schemas/pages/schema/field-wizard.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts b/frontend/app/features/schemas/pages/schema/field-wizard.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts rename to frontend/app/features/schemas/pages/schema/field-wizard.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/frontend/app/features/schemas/pages/schema/field.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field.component.html rename to frontend/app/features/schemas/pages/schema/field.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.scss b/frontend/app/features/schemas/pages/schema/field.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field.component.scss rename to frontend/app/features/schemas/pages/schema/field.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/frontend/app/features/schemas/pages/schema/field.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/field.component.ts rename to frontend/app/features/schemas/pages/schema/field.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form-common.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form-common.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form.component.ts b/frontend/app/features/schemas/pages/schema/forms/field-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/forms/field-form.component.ts rename to frontend/app/features/schemas/pages/schema/forms/field-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-edit-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-edit-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html b/frontend/app/features/schemas/pages/schema/schema-export-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-export-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-export-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-export-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-export-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-export-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/frontend/app/features/schemas/pages/schema/schema-page.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-page.component.html rename to frontend/app/features/schemas/pages/schema/schema-page.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss b/frontend/app/features/schemas/pages/schema/schema-page.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss rename to frontend/app/features/schemas/pages/schema/schema-page.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/frontend/app/features/schemas/pages/schema/schema-page.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts rename to frontend/app/features/schemas/pages/schema/schema-page.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html rename to frontend/app/features/schemas/pages/schema/schema-scripts-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss rename to frontend/app/features/schemas/pages/schema/schema-scripts-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts rename to frontend/app/features/schemas/pages/schema/schema-scripts-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.html b/frontend/app/features/schemas/pages/schema/types/array-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/array-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/array-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/array-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/array-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/array-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/array-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.html b/frontend/app/features/schemas/pages/schema/types/assets-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/assets-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/assets-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/assets-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/assets-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/assets-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html b/frontend/app/features/schemas/pages/schema/types/assets-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/assets-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/assets-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/assets-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/assets-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/assets-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/assets-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html b/frontend/app/features/schemas/pages/schema/types/boolean-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/boolean-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/boolean-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/boolean-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/boolean-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/boolean-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html b/frontend/app/features/schemas/pages/schema/types/boolean-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/boolean-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/boolean-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/boolean-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/boolean-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/boolean-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html b/frontend/app/features/schemas/pages/schema/types/date-time-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/date-time-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/date-time-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/date-time-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/date-time-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/date-time-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html b/frontend/app/features/schemas/pages/schema/types/date-time-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/date-time-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/date-time-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/date-time-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/date-time-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/date-time-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html b/frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/geolocation-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html b/frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/geolocation-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html b/frontend/app/features/schemas/pages/schema/types/json-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/json-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/json-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/json-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/json-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/json-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.html b/frontend/app/features/schemas/pages/schema/types/json-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/json-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/json-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/json-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/json-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/json-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/json-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html b/frontend/app/features/schemas/pages/schema/types/number-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/number-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/number-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/number-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/number-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/number-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html b/frontend/app/features/schemas/pages/schema/types/number-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/number-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/number-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/number-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/number-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/number-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.html b/frontend/app/features/schemas/pages/schema/types/references-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/references-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/references-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/references-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/references-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/references-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html b/frontend/app/features/schemas/pages/schema/types/references-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/references-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/references-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/references-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/references-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/references-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html b/frontend/app/features/schemas/pages/schema/types/string-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/string-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/string-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/string-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/string-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/string-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html b/frontend/app/features/schemas/pages/schema/types/string-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/string-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/string-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/string-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/string-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/string-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.html b/frontend/app/features/schemas/pages/schema/types/tags-ui.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.html rename to frontend/app/features/schemas/pages/schema/types/tags-ui.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.scss b/frontend/app/features/schemas/pages/schema/types/tags-ui.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.scss rename to frontend/app/features/schemas/pages/schema/types/tags-ui.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.ts b/frontend/app/features/schemas/pages/schema/types/tags-ui.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-ui.component.ts rename to frontend/app/features/schemas/pages/schema/types/tags-ui.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.html b/frontend/app/features/schemas/pages/schema/types/tags-validation.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.html rename to frontend/app/features/schemas/pages/schema/types/tags-validation.component.html diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.scss b/frontend/app/features/schemas/pages/schema/types/tags-validation.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.scss rename to frontend/app/features/schemas/pages/schema/types/tags-validation.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/tags-validation.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schema/types/tags-validation.component.ts rename to frontend/app/features/schemas/pages/schema/types/tags-validation.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/frontend/app/features/schemas/pages/schemas/schema-form.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html rename to frontend/app/features/schemas/pages/schemas/schema-form.component.html diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.scss b/frontend/app/features/schemas/pages/schemas/schema-form.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schema-form.component.scss rename to frontend/app/features/schemas/pages/schemas/schema-form.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/frontend/app/features/schemas/pages/schemas/schema-form.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts rename to frontend/app/features/schemas/pages/schemas/schema-form.component.ts diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/frontend/app/features/schemas/pages/schemas/schemas-page.component.html similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html rename to frontend/app/features/schemas/pages/schemas/schemas-page.component.html diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss b/frontend/app/features/schemas/pages/schemas/schemas-page.component.scss similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss rename to frontend/app/features/schemas/pages/schemas/schemas-page.component.scss diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/frontend/app/features/schemas/pages/schemas/schemas-page.component.ts similarity index 100% rename from src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts rename to frontend/app/features/schemas/pages/schemas/schemas-page.component.ts diff --git a/src/Squidex/app/features/settings/declarations.ts b/frontend/app/features/settings/declarations.ts similarity index 100% rename from src/Squidex/app/features/settings/declarations.ts rename to frontend/app/features/settings/declarations.ts diff --git a/src/Squidex/app/features/settings/index.ts b/frontend/app/features/settings/index.ts similarity index 100% rename from src/Squidex/app/features/settings/index.ts rename to frontend/app/features/settings/index.ts diff --git a/src/Squidex/app/features/settings/module.ts b/frontend/app/features/settings/module.ts similarity index 100% rename from src/Squidex/app/features/settings/module.ts rename to frontend/app/features/settings/module.ts diff --git a/src/Squidex/app/features/settings/pages/backups/backup.component.ts b/frontend/app/features/settings/pages/backups/backup.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backup.component.ts rename to frontend/app/features/settings/pages/backups/backup.component.ts diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/frontend/app/features/settings/pages/backups/backups-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backups-page.component.html rename to frontend/app/features/settings/pages/backups/backups-page.component.html diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.scss b/frontend/app/features/settings/pages/backups/backups-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backups-page.component.scss rename to frontend/app/features/settings/pages/backups/backups-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/frontend/app/features/settings/pages/backups/backups-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/backups/backups-page.component.ts rename to frontend/app/features/settings/pages/backups/backups-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/clients/client-add-form.component.ts b/frontend/app/features/settings/pages/clients/client-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client-add-form.component.ts rename to frontend/app/features/settings/pages/clients/client-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/frontend/app/features/settings/pages/clients/client.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client.component.html rename to frontend/app/features/settings/pages/clients/client.component.html diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.scss b/frontend/app/features/settings/pages/clients/client.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client.component.scss rename to frontend/app/features/settings/pages/clients/client.component.scss diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.ts b/frontend/app/features/settings/pages/clients/client.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/client.component.ts rename to frontend/app/features/settings/pages/clients/client.component.ts diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/frontend/app/features/settings/pages/clients/clients-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/clients-page.component.html rename to frontend/app/features/settings/pages/clients/clients-page.component.html diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.scss b/frontend/app/features/settings/pages/clients/clients-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/clients-page.component.scss rename to frontend/app/features/settings/pages/clients/clients-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/frontend/app/features/settings/pages/clients/clients-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/clients/clients-page.component.ts rename to frontend/app/features/settings/pages/clients/clients-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.html b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.html rename to frontend/app/features/settings/pages/contributors/contributor-add-form.component.html diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.scss b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.scss rename to frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.ts b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.ts rename to frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/contributor.component.ts b/frontend/app/features/settings/pages/contributors/contributor.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributor.component.ts rename to frontend/app/features/settings/pages/contributors/contributor.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/frontend/app/features/settings/pages/contributors/contributors-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html rename to frontend/app/features/settings/pages/contributors/contributors-page.component.html diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss b/frontend/app/features/settings/pages/contributors/contributors-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss rename to frontend/app/features/settings/pages/contributors/contributors-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/frontend/app/features/settings/pages/contributors/contributors-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts rename to frontend/app/features/settings/pages/contributors/contributors-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.html rename to frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.html diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.scss rename to frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.scss diff --git a/src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts rename to frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts diff --git a/src/Squidex/app/features/settings/pages/languages/language-add-form.component.ts b/frontend/app/features/settings/pages/languages/language-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language-add-form.component.ts rename to frontend/app/features/settings/pages/languages/language-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.html b/frontend/app/features/settings/pages/languages/language.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language.component.html rename to frontend/app/features/settings/pages/languages/language.component.html diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.scss b/frontend/app/features/settings/pages/languages/language.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language.component.scss rename to frontend/app/features/settings/pages/languages/language.component.scss diff --git a/src/Squidex/app/features/settings/pages/languages/language.component.ts b/frontend/app/features/settings/pages/languages/language.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/language.component.ts rename to frontend/app/features/settings/pages/languages/language.component.ts diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/frontend/app/features/settings/pages/languages/languages-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/languages-page.component.html rename to frontend/app/features/settings/pages/languages/languages-page.component.html diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.scss b/frontend/app/features/settings/pages/languages/languages-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/languages-page.component.scss rename to frontend/app/features/settings/pages/languages/languages-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/frontend/app/features/settings/pages/languages/languages-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/languages/languages-page.component.ts rename to frontend/app/features/settings/pages/languages/languages-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.html b/frontend/app/features/settings/pages/more/more-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/more/more-page.component.html rename to frontend/app/features/settings/pages/more/more-page.component.html diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.scss b/frontend/app/features/settings/pages/more/more-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/more/more-page.component.scss rename to frontend/app/features/settings/pages/more/more-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.ts b/frontend/app/features/settings/pages/more/more-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/more/more-page.component.ts rename to frontend/app/features/settings/pages/more/more-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.html b/frontend/app/features/settings/pages/patterns/pattern.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/pattern.component.html rename to frontend/app/features/settings/pages/patterns/pattern.component.html diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.scss b/frontend/app/features/settings/pages/patterns/pattern.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/pattern.component.scss rename to frontend/app/features/settings/pages/patterns/pattern.component.scss diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.ts b/frontend/app/features/settings/pages/patterns/pattern.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/pattern.component.ts rename to frontend/app/features/settings/pages/patterns/pattern.component.ts diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html b/frontend/app/features/settings/pages/patterns/patterns-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html rename to frontend/app/features/settings/pages/patterns/patterns-page.component.html diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss b/frontend/app/features/settings/pages/patterns/patterns-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss rename to frontend/app/features/settings/pages/patterns/patterns-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts b/frontend/app/features/settings/pages/patterns/patterns-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts rename to frontend/app/features/settings/pages/patterns/patterns-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/plans/plan.component.html b/frontend/app/features/settings/pages/plans/plan.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plan.component.html rename to frontend/app/features/settings/pages/plans/plan.component.html diff --git a/src/Squidex/app/features/settings/pages/plans/plan.component.scss b/frontend/app/features/settings/pages/plans/plan.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plan.component.scss rename to frontend/app/features/settings/pages/plans/plan.component.scss diff --git a/src/Squidex/app/features/settings/pages/plans/plan.component.ts b/frontend/app/features/settings/pages/plans/plan.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plan.component.ts rename to frontend/app/features/settings/pages/plans/plan.component.ts diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/frontend/app/features/settings/pages/plans/plans-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plans-page.component.html rename to frontend/app/features/settings/pages/plans/plans-page.component.html diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.scss b/frontend/app/features/settings/pages/plans/plans-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plans-page.component.scss rename to frontend/app/features/settings/pages/plans/plans-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts b/frontend/app/features/settings/pages/plans/plans-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/plans/plans-page.component.ts rename to frontend/app/features/settings/pages/plans/plans-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/roles/role-add-form.component.ts b/frontend/app/features/settings/pages/roles/role-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role-add-form.component.ts rename to frontend/app/features/settings/pages/roles/role-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.html b/frontend/app/features/settings/pages/roles/role.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role.component.html rename to frontend/app/features/settings/pages/roles/role.component.html diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.scss b/frontend/app/features/settings/pages/roles/role.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role.component.scss rename to frontend/app/features/settings/pages/roles/role.component.scss diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.ts b/frontend/app/features/settings/pages/roles/role.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/role.component.ts rename to frontend/app/features/settings/pages/roles/role.component.ts diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html b/frontend/app/features/settings/pages/roles/roles-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/roles-page.component.html rename to frontend/app/features/settings/pages/roles/roles-page.component.html diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.scss b/frontend/app/features/settings/pages/roles/roles-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/roles-page.component.scss rename to frontend/app/features/settings/pages/roles/roles-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.ts b/frontend/app/features/settings/pages/roles/roles-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/roles/roles-page.component.ts rename to frontend/app/features/settings/pages/roles/roles-page.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-add-form.component.ts b/frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-add-form.component.ts rename to frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html b/frontend/app/features/settings/pages/workflows/workflow-step.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html rename to frontend/app/features/settings/pages/workflows/workflow-step.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.scss b/frontend/app/features/settings/pages/workflows/workflow-step.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-step.component.scss rename to frontend/app/features/settings/pages/workflows/workflow-step.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts b/frontend/app/features/settings/pages/workflows/workflow-step.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts rename to frontend/app/features/settings/pages/workflows/workflow-step.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.html b/frontend/app/features/settings/pages/workflows/workflow-transition.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.html rename to frontend/app/features/settings/pages/workflows/workflow-transition.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.scss b/frontend/app/features/settings/pages/workflows/workflow-transition.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.scss rename to frontend/app/features/settings/pages/workflows/workflow-transition.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.ts b/frontend/app/features/settings/pages/workflows/workflow-transition.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.ts rename to frontend/app/features/settings/pages/workflows/workflow-transition.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html b/frontend/app/features/settings/pages/workflows/workflow.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow.component.html rename to frontend/app/features/settings/pages/workflows/workflow.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss b/frontend/app/features/settings/pages/workflows/workflow.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow.component.scss rename to frontend/app/features/settings/pages/workflows/workflow.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts b/frontend/app/features/settings/pages/workflows/workflow.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflow.component.ts rename to frontend/app/features/settings/pages/workflows/workflow.component.ts diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html b/frontend/app/features/settings/pages/workflows/workflows-page.component.html similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html rename to frontend/app/features/settings/pages/workflows/workflows-page.component.html diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss b/frontend/app/features/settings/pages/workflows/workflows-page.component.scss similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss rename to frontend/app/features/settings/pages/workflows/workflows-page.component.scss diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts b/frontend/app/features/settings/pages/workflows/workflows-page.component.ts similarity index 100% rename from src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts rename to frontend/app/features/settings/pages/workflows/workflows-page.component.ts diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/frontend/app/features/settings/settings-area.component.html similarity index 100% rename from src/Squidex/app/features/settings/settings-area.component.html rename to frontend/app/features/settings/settings-area.component.html diff --git a/src/Squidex/app/features/settings/settings-area.component.scss b/frontend/app/features/settings/settings-area.component.scss similarity index 100% rename from src/Squidex/app/features/settings/settings-area.component.scss rename to frontend/app/features/settings/settings-area.component.scss diff --git a/src/Squidex/app/features/settings/settings-area.component.ts b/frontend/app/features/settings/settings-area.component.ts similarity index 100% rename from src/Squidex/app/features/settings/settings-area.component.ts rename to frontend/app/features/settings/settings-area.component.ts diff --git a/src/Squidex/app/framework/angular/animations.ts b/frontend/app/framework/angular/animations.ts similarity index 100% rename from src/Squidex/app/framework/angular/animations.ts rename to frontend/app/framework/angular/animations.ts diff --git a/src/Squidex/app/framework/angular/avatar.component.ts b/frontend/app/framework/angular/avatar.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/avatar.component.ts rename to frontend/app/framework/angular/avatar.component.ts diff --git a/src/Squidex/app/framework/angular/code.component.html b/frontend/app/framework/angular/code.component.html similarity index 100% rename from src/Squidex/app/framework/angular/code.component.html rename to frontend/app/framework/angular/code.component.html diff --git a/src/Squidex/app/framework/angular/code.component.scss b/frontend/app/framework/angular/code.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/code.component.scss rename to frontend/app/framework/angular/code.component.scss diff --git a/src/Squidex/app/framework/angular/code.component.ts b/frontend/app/framework/angular/code.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/code.component.ts rename to frontend/app/framework/angular/code.component.ts diff --git a/src/Squidex/app/framework/angular/drag-helper.ts b/frontend/app/framework/angular/drag-helper.ts similarity index 100% rename from src/Squidex/app/framework/angular/drag-helper.ts rename to frontend/app/framework/angular/drag-helper.ts diff --git a/src/Squidex/app/framework/angular/external-link.directive.ts b/frontend/app/framework/angular/external-link.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/external-link.directive.ts rename to frontend/app/framework/angular/external-link.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/frontend/app/framework/angular/forms/autocomplete.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/autocomplete.component.html rename to frontend/app/framework/angular/forms/autocomplete.component.html diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.scss b/frontend/app/framework/angular/forms/autocomplete.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/autocomplete.component.scss rename to frontend/app/framework/angular/forms/autocomplete.component.scss diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/frontend/app/framework/angular/forms/autocomplete.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/autocomplete.component.ts rename to frontend/app/framework/angular/forms/autocomplete.component.ts diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html b/frontend/app/framework/angular/forms/checkbox-group.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/checkbox-group.component.html rename to frontend/app/framework/angular/forms/checkbox-group.component.html diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.scss b/frontend/app/framework/angular/forms/checkbox-group.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/checkbox-group.component.scss rename to frontend/app/framework/angular/forms/checkbox-group.component.scss diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts b/frontend/app/framework/angular/forms/checkbox-group.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/checkbox-group.component.ts rename to frontend/app/framework/angular/forms/checkbox-group.component.ts diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.html b/frontend/app/framework/angular/forms/code-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/code-editor.component.html rename to frontend/app/framework/angular/forms/code-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.scss b/frontend/app/framework/angular/forms/code-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/code-editor.component.scss rename to frontend/app/framework/angular/forms/code-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/frontend/app/framework/angular/forms/code-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/code-editor.component.ts rename to frontend/app/framework/angular/forms/code-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.html b/frontend/app/framework/angular/forms/color-picker.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/color-picker.component.html rename to frontend/app/framework/angular/forms/color-picker.component.html diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.scss b/frontend/app/framework/angular/forms/color-picker.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/color-picker.component.scss rename to frontend/app/framework/angular/forms/color-picker.component.scss diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.ts b/frontend/app/framework/angular/forms/color-picker.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/color-picker.component.ts rename to frontend/app/framework/angular/forms/color-picker.component.ts diff --git a/src/Squidex/app/framework/angular/forms/confirm-click.directive.ts b/frontend/app/framework/angular/forms/confirm-click.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/confirm-click.directive.ts rename to frontend/app/framework/angular/forms/confirm-click.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.html b/frontend/app/framework/angular/forms/control-errors.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/control-errors.component.html rename to frontend/app/framework/angular/forms/control-errors.component.html diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.scss b/frontend/app/framework/angular/forms/control-errors.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/control-errors.component.scss rename to frontend/app/framework/angular/forms/control-errors.component.scss diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/frontend/app/framework/angular/forms/control-errors.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/control-errors.component.ts rename to frontend/app/framework/angular/forms/control-errors.component.ts diff --git a/src/Squidex/app/framework/angular/forms/copy.directive.ts b/frontend/app/framework/angular/forms/copy.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/copy.directive.ts rename to frontend/app/framework/angular/forms/copy.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.html b/frontend/app/framework/angular/forms/date-time-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/date-time-editor.component.html rename to frontend/app/framework/angular/forms/date-time-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.scss b/frontend/app/framework/angular/forms/date-time-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/date-time-editor.component.scss rename to frontend/app/framework/angular/forms/date-time-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/frontend/app/framework/angular/forms/date-time-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/date-time-editor.component.ts rename to frontend/app/framework/angular/forms/date-time-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.html b/frontend/app/framework/angular/forms/dropdown.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/dropdown.component.html rename to frontend/app/framework/angular/forms/dropdown.component.html diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.scss b/frontend/app/framework/angular/forms/dropdown.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/dropdown.component.scss rename to frontend/app/framework/angular/forms/dropdown.component.scss diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.ts b/frontend/app/framework/angular/forms/dropdown.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/dropdown.component.ts rename to frontend/app/framework/angular/forms/dropdown.component.ts diff --git a/src/Squidex/app/framework/angular/forms/editable-title.component.html b/frontend/app/framework/angular/forms/editable-title.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/editable-title.component.html rename to frontend/app/framework/angular/forms/editable-title.component.html diff --git a/src/Squidex/app/framework/angular/forms/editable-title.component.scss b/frontend/app/framework/angular/forms/editable-title.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/editable-title.component.scss rename to frontend/app/framework/angular/forms/editable-title.component.scss diff --git a/src/Squidex/app/framework/angular/forms/editable-title.component.ts b/frontend/app/framework/angular/forms/editable-title.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/editable-title.component.ts rename to frontend/app/framework/angular/forms/editable-title.component.ts diff --git a/src/Squidex/app/framework/angular/forms/error-formatting.spec.ts b/frontend/app/framework/angular/forms/error-formatting.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/error-formatting.spec.ts rename to frontend/app/framework/angular/forms/error-formatting.spec.ts diff --git a/src/Squidex/app/framework/angular/forms/error-formatting.ts b/frontend/app/framework/angular/forms/error-formatting.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/error-formatting.ts rename to frontend/app/framework/angular/forms/error-formatting.ts diff --git a/src/Squidex/app/framework/angular/forms/file-drop.directive.ts b/frontend/app/framework/angular/forms/file-drop.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/file-drop.directive.ts rename to frontend/app/framework/angular/forms/file-drop.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts b/frontend/app/framework/angular/forms/focus-on-init.directive.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/focus-on-init.directive.spec.ts rename to frontend/app/framework/angular/forms/focus-on-init.directive.spec.ts diff --git a/src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts b/frontend/app/framework/angular/forms/focus-on-init.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/focus-on-init.directive.ts rename to frontend/app/framework/angular/forms/focus-on-init.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/form-alert.component.ts b/frontend/app/framework/angular/forms/form-alert.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/form-alert.component.ts rename to frontend/app/framework/angular/forms/form-alert.component.ts diff --git a/src/Squidex/app/framework/angular/forms/form-error.component.ts b/frontend/app/framework/angular/forms/form-error.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/form-error.component.ts rename to frontend/app/framework/angular/forms/form-error.component.ts diff --git a/src/Squidex/app/framework/angular/forms/form-hint.component.ts b/frontend/app/framework/angular/forms/form-hint.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/form-hint.component.ts rename to frontend/app/framework/angular/forms/form-hint.component.ts diff --git a/src/Squidex/app/framework/angular/forms/forms-helper.ts b/frontend/app/framework/angular/forms/forms-helper.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/forms-helper.ts rename to frontend/app/framework/angular/forms/forms-helper.ts diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.html b/frontend/app/framework/angular/forms/iframe-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/iframe-editor.component.html rename to frontend/app/framework/angular/forms/iframe-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.scss b/frontend/app/framework/angular/forms/iframe-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/iframe-editor.component.scss rename to frontend/app/framework/angular/forms/iframe-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/frontend/app/framework/angular/forms/iframe-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/iframe-editor.component.ts rename to frontend/app/framework/angular/forms/iframe-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts b/frontend/app/framework/angular/forms/indeterminate-value.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/indeterminate-value.directive.ts rename to frontend/app/framework/angular/forms/indeterminate-value.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.html b/frontend/app/framework/angular/forms/json-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/json-editor.component.html rename to frontend/app/framework/angular/forms/json-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.scss b/frontend/app/framework/angular/forms/json-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/json-editor.component.scss rename to frontend/app/framework/angular/forms/json-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/frontend/app/framework/angular/forms/json-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/json-editor.component.ts rename to frontend/app/framework/angular/forms/json-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts b/frontend/app/framework/angular/forms/progress-bar.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/progress-bar.component.ts rename to frontend/app/framework/angular/forms/progress-bar.component.ts diff --git a/src/Squidex/app/framework/angular/forms/stars.component.html b/frontend/app/framework/angular/forms/stars.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/stars.component.html rename to frontend/app/framework/angular/forms/stars.component.html diff --git a/src/Squidex/app/framework/angular/forms/stars.component.scss b/frontend/app/framework/angular/forms/stars.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/stars.component.scss rename to frontend/app/framework/angular/forms/stars.component.scss diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/frontend/app/framework/angular/forms/stars.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/stars.component.ts rename to frontend/app/framework/angular/forms/stars.component.ts diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/frontend/app/framework/angular/forms/tag-editor.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/tag-editor.component.html rename to frontend/app/framework/angular/forms/tag-editor.component.html diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.scss b/frontend/app/framework/angular/forms/tag-editor.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/tag-editor.component.scss rename to frontend/app/framework/angular/forms/tag-editor.component.scss diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/frontend/app/framework/angular/forms/tag-editor.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/tag-editor.component.ts rename to frontend/app/framework/angular/forms/tag-editor.component.ts diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.html b/frontend/app/framework/angular/forms/toggle.component.html similarity index 100% rename from src/Squidex/app/framework/angular/forms/toggle.component.html rename to frontend/app/framework/angular/forms/toggle.component.html diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.scss b/frontend/app/framework/angular/forms/toggle.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/forms/toggle.component.scss rename to frontend/app/framework/angular/forms/toggle.component.scss diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/frontend/app/framework/angular/forms/toggle.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/toggle.component.ts rename to frontend/app/framework/angular/forms/toggle.component.ts diff --git a/src/Squidex/app/framework/angular/forms/transform-input.directive.ts b/frontend/app/framework/angular/forms/transform-input.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/transform-input.directive.ts rename to frontend/app/framework/angular/forms/transform-input.directive.ts diff --git a/src/Squidex/app/framework/angular/forms/validators.spec.ts b/frontend/app/framework/angular/forms/validators.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/validators.spec.ts rename to frontend/app/framework/angular/forms/validators.spec.ts diff --git a/src/Squidex/app/framework/angular/forms/validators.ts b/frontend/app/framework/angular/forms/validators.ts similarity index 100% rename from src/Squidex/app/framework/angular/forms/validators.ts rename to frontend/app/framework/angular/forms/validators.ts diff --git a/src/Squidex/app/framework/angular/highlight.pipe.ts b/frontend/app/framework/angular/highlight.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/highlight.pipe.ts rename to frontend/app/framework/angular/highlight.pipe.ts diff --git a/src/Squidex/app/framework/angular/hover-background.directive.ts b/frontend/app/framework/angular/hover-background.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/hover-background.directive.ts rename to frontend/app/framework/angular/hover-background.directive.ts diff --git a/src/Squidex/app/framework/angular/http/caching.interceptor.ts b/frontend/app/framework/angular/http/caching.interceptor.ts similarity index 100% rename from src/Squidex/app/framework/angular/http/caching.interceptor.ts rename to frontend/app/framework/angular/http/caching.interceptor.ts diff --git a/src/Squidex/app/framework/angular/http/http-extensions.ts b/frontend/app/framework/angular/http/http-extensions.ts similarity index 100% rename from src/Squidex/app/framework/angular/http/http-extensions.ts rename to frontend/app/framework/angular/http/http-extensions.ts diff --git a/src/Squidex/app/framework/angular/http/loading.interceptor.ts b/frontend/app/framework/angular/http/loading.interceptor.ts similarity index 100% rename from src/Squidex/app/framework/angular/http/loading.interceptor.ts rename to frontend/app/framework/angular/http/loading.interceptor.ts diff --git a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts b/frontend/app/framework/angular/ignore-scrollbar.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts rename to frontend/app/framework/angular/ignore-scrollbar.directive.ts diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/frontend/app/framework/angular/image-source.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/image-source.directive.ts rename to frontend/app/framework/angular/image-source.directive.ts diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html b/frontend/app/framework/angular/modals/dialog-renderer.component.html similarity index 100% rename from src/Squidex/app/framework/angular/modals/dialog-renderer.component.html rename to frontend/app/framework/angular/modals/dialog-renderer.component.html diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss b/frontend/app/framework/angular/modals/dialog-renderer.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss rename to frontend/app/framework/angular/modals/dialog-renderer.component.scss diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/frontend/app/framework/angular/modals/dialog-renderer.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts rename to frontend/app/framework/angular/modals/dialog-renderer.component.ts diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.html b/frontend/app/framework/angular/modals/modal-dialog.component.html similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-dialog.component.html rename to frontend/app/framework/angular/modals/modal-dialog.component.html diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.scss b/frontend/app/framework/angular/modals/modal-dialog.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-dialog.component.scss rename to frontend/app/framework/angular/modals/modal-dialog.component.scss diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.ts b/frontend/app/framework/angular/modals/modal-dialog.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-dialog.component.ts rename to frontend/app/framework/angular/modals/modal-dialog.component.ts diff --git a/src/Squidex/app/framework/angular/modals/modal-placement.directive.ts b/frontend/app/framework/angular/modals/modal-placement.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal-placement.directive.ts rename to frontend/app/framework/angular/modals/modal-placement.directive.ts diff --git a/src/Squidex/app/framework/angular/modals/modal.directive.ts b/frontend/app/framework/angular/modals/modal.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/modal.directive.ts rename to frontend/app/framework/angular/modals/modal.directive.ts diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.html b/frontend/app/framework/angular/modals/onboarding-tooltip.component.html similarity index 100% rename from src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.html rename to frontend/app/framework/angular/modals/onboarding-tooltip.component.html diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.scss b/frontend/app/framework/angular/modals/onboarding-tooltip.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.scss rename to frontend/app/framework/angular/modals/onboarding-tooltip.component.scss diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts b/frontend/app/framework/angular/modals/onboarding-tooltip.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts rename to frontend/app/framework/angular/modals/onboarding-tooltip.component.ts diff --git a/src/Squidex/app/framework/angular/modals/root-view.component.ts b/frontend/app/framework/angular/modals/root-view.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/root-view.component.ts rename to frontend/app/framework/angular/modals/root-view.component.ts diff --git a/src/Squidex/app/framework/angular/modals/tooltip.directive.ts b/frontend/app/framework/angular/modals/tooltip.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/modals/tooltip.directive.ts rename to frontend/app/framework/angular/modals/tooltip.directive.ts diff --git a/src/Squidex/app/framework/angular/pager.component.html b/frontend/app/framework/angular/pager.component.html similarity index 100% rename from src/Squidex/app/framework/angular/pager.component.html rename to frontend/app/framework/angular/pager.component.html diff --git a/src/Squidex/app/framework/angular/pager.component.scss b/frontend/app/framework/angular/pager.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/pager.component.scss rename to frontend/app/framework/angular/pager.component.scss diff --git a/src/Squidex/app/framework/angular/pager.component.ts b/frontend/app/framework/angular/pager.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/pager.component.ts rename to frontend/app/framework/angular/pager.component.ts diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/frontend/app/framework/angular/panel-container.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/panel-container.directive.ts rename to frontend/app/framework/angular/panel-container.directive.ts diff --git a/src/Squidex/app/framework/angular/panel.component.html b/frontend/app/framework/angular/panel.component.html similarity index 100% rename from src/Squidex/app/framework/angular/panel.component.html rename to frontend/app/framework/angular/panel.component.html diff --git a/src/Squidex/app/framework/angular/panel.component.scss b/frontend/app/framework/angular/panel.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/panel.component.scss rename to frontend/app/framework/angular/panel.component.scss diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/frontend/app/framework/angular/panel.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/panel.component.ts rename to frontend/app/framework/angular/panel.component.ts diff --git a/src/Squidex/app/framework/angular/pipes/colors.pipes.spec.ts b/frontend/app/framework/angular/pipes/colors.pipes.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/colors.pipes.spec.ts rename to frontend/app/framework/angular/pipes/colors.pipes.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/colors.pipes.ts b/frontend/app/framework/angular/pipes/colors.pipes.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/colors.pipes.ts rename to frontend/app/framework/angular/pipes/colors.pipes.ts diff --git a/src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts b/frontend/app/framework/angular/pipes/date-time.pipes.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts rename to frontend/app/framework/angular/pipes/date-time.pipes.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/date-time.pipes.ts b/frontend/app/framework/angular/pipes/date-time.pipes.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/date-time.pipes.ts rename to frontend/app/framework/angular/pipes/date-time.pipes.ts diff --git a/src/Squidex/app/framework/angular/pipes/keys.pipe.spec.ts b/frontend/app/framework/angular/pipes/keys.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/keys.pipe.spec.ts rename to frontend/app/framework/angular/pipes/keys.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/keys.pipe.ts b/frontend/app/framework/angular/pipes/keys.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/keys.pipe.ts rename to frontend/app/framework/angular/pipes/keys.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/markdown.pipe.spec.ts b/frontend/app/framework/angular/pipes/markdown.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/markdown.pipe.spec.ts rename to frontend/app/framework/angular/pipes/markdown.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/markdown.pipe.ts b/frontend/app/framework/angular/pipes/markdown.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/markdown.pipe.ts rename to frontend/app/framework/angular/pipes/markdown.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/money.pipe.spec.ts b/frontend/app/framework/angular/pipes/money.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/money.pipe.spec.ts rename to frontend/app/framework/angular/pipes/money.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/money.pipe.ts b/frontend/app/framework/angular/pipes/money.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/money.pipe.ts rename to frontend/app/framework/angular/pipes/money.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/name.pipe.spec.ts b/frontend/app/framework/angular/pipes/name.pipe.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/name.pipe.spec.ts rename to frontend/app/framework/angular/pipes/name.pipe.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/name.pipe.ts b/frontend/app/framework/angular/pipes/name.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/name.pipe.ts rename to frontend/app/framework/angular/pipes/name.pipe.ts diff --git a/src/Squidex/app/framework/angular/pipes/numbers.pipes.spec.ts b/frontend/app/framework/angular/pipes/numbers.pipes.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/numbers.pipes.spec.ts rename to frontend/app/framework/angular/pipes/numbers.pipes.spec.ts diff --git a/src/Squidex/app/framework/angular/pipes/numbers.pipes.ts b/frontend/app/framework/angular/pipes/numbers.pipes.ts similarity index 100% rename from src/Squidex/app/framework/angular/pipes/numbers.pipes.ts rename to frontend/app/framework/angular/pipes/numbers.pipes.ts diff --git a/src/Squidex/app/framework/angular/popup-link.directive.ts b/frontend/app/framework/angular/popup-link.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/popup-link.directive.ts rename to frontend/app/framework/angular/popup-link.directive.ts diff --git a/src/Squidex/app/framework/angular/routers/can-deactivate.guard.spec.ts b/frontend/app/framework/angular/routers/can-deactivate.guard.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/can-deactivate.guard.spec.ts rename to frontend/app/framework/angular/routers/can-deactivate.guard.spec.ts diff --git a/src/Squidex/app/framework/angular/routers/can-deactivate.guard.ts b/frontend/app/framework/angular/routers/can-deactivate.guard.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/can-deactivate.guard.ts rename to frontend/app/framework/angular/routers/can-deactivate.guard.ts diff --git a/src/Squidex/app/framework/angular/routers/parent-link.directive.ts b/frontend/app/framework/angular/routers/parent-link.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/parent-link.directive.ts rename to frontend/app/framework/angular/routers/parent-link.directive.ts diff --git a/src/Squidex/app/framework/angular/routers/router-utils.spec.ts b/frontend/app/framework/angular/routers/router-utils.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/router-utils.spec.ts rename to frontend/app/framework/angular/routers/router-utils.spec.ts diff --git a/src/Squidex/app/framework/angular/routers/router-utils.ts b/frontend/app/framework/angular/routers/router-utils.ts similarity index 100% rename from src/Squidex/app/framework/angular/routers/router-utils.ts rename to frontend/app/framework/angular/routers/router-utils.ts diff --git a/src/Squidex/app/framework/angular/safe-html.pipe.ts b/frontend/app/framework/angular/safe-html.pipe.ts similarity index 100% rename from src/Squidex/app/framework/angular/safe-html.pipe.ts rename to frontend/app/framework/angular/safe-html.pipe.ts diff --git a/src/Squidex/app/framework/angular/scroll-active.directive.ts b/frontend/app/framework/angular/scroll-active.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/scroll-active.directive.ts rename to frontend/app/framework/angular/scroll-active.directive.ts diff --git a/src/Squidex/app/framework/angular/shortcut.component.spec.ts b/frontend/app/framework/angular/shortcut.component.spec.ts similarity index 100% rename from src/Squidex/app/framework/angular/shortcut.component.spec.ts rename to frontend/app/framework/angular/shortcut.component.spec.ts diff --git a/src/Squidex/app/framework/angular/shortcut.component.ts b/frontend/app/framework/angular/shortcut.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/shortcut.component.ts rename to frontend/app/framework/angular/shortcut.component.ts diff --git a/src/Squidex/app/framework/angular/stateful.component.ts b/frontend/app/framework/angular/stateful.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/stateful.component.ts rename to frontend/app/framework/angular/stateful.component.ts diff --git a/src/Squidex/app/framework/angular/status-icon.component.html b/frontend/app/framework/angular/status-icon.component.html similarity index 100% rename from src/Squidex/app/framework/angular/status-icon.component.html rename to frontend/app/framework/angular/status-icon.component.html diff --git a/src/Squidex/app/framework/angular/status-icon.component.scss b/frontend/app/framework/angular/status-icon.component.scss similarity index 100% rename from src/Squidex/app/framework/angular/status-icon.component.scss rename to frontend/app/framework/angular/status-icon.component.scss diff --git a/src/Squidex/app/framework/angular/status-icon.component.ts b/frontend/app/framework/angular/status-icon.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/status-icon.component.ts rename to frontend/app/framework/angular/status-icon.component.ts diff --git a/src/Squidex/app/framework/angular/stop-click.directive.ts b/frontend/app/framework/angular/stop-click.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/stop-click.directive.ts rename to frontend/app/framework/angular/stop-click.directive.ts diff --git a/src/Squidex/app/framework/angular/sync-scrolling.directive.ts b/frontend/app/framework/angular/sync-scrolling.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/sync-scrolling.directive.ts rename to frontend/app/framework/angular/sync-scrolling.directive.ts diff --git a/src/Squidex/app/framework/angular/template-wrapper.directive.ts b/frontend/app/framework/angular/template-wrapper.directive.ts similarity index 100% rename from src/Squidex/app/framework/angular/template-wrapper.directive.ts rename to frontend/app/framework/angular/template-wrapper.directive.ts diff --git a/src/Squidex/app/framework/angular/title.component.ts b/frontend/app/framework/angular/title.component.ts similarity index 100% rename from src/Squidex/app/framework/angular/title.component.ts rename to frontend/app/framework/angular/title.component.ts diff --git a/src/Squidex/app/framework/configurations.ts b/frontend/app/framework/configurations.ts similarity index 100% rename from src/Squidex/app/framework/configurations.ts rename to frontend/app/framework/configurations.ts diff --git a/src/Squidex/app/framework/declarations.ts b/frontend/app/framework/declarations.ts similarity index 100% rename from src/Squidex/app/framework/declarations.ts rename to frontend/app/framework/declarations.ts diff --git a/src/Squidex/app/framework/index.ts b/frontend/app/framework/index.ts similarity index 100% rename from src/Squidex/app/framework/index.ts rename to frontend/app/framework/index.ts diff --git a/src/Squidex/app/framework/internal.ts b/frontend/app/framework/internal.ts similarity index 100% rename from src/Squidex/app/framework/internal.ts rename to frontend/app/framework/internal.ts diff --git a/src/Squidex/app/framework/module.ts b/frontend/app/framework/module.ts similarity index 100% rename from src/Squidex/app/framework/module.ts rename to frontend/app/framework/module.ts diff --git a/src/Squidex/app/framework/services/analytics.service.ts b/frontend/app/framework/services/analytics.service.ts similarity index 100% rename from src/Squidex/app/framework/services/analytics.service.ts rename to frontend/app/framework/services/analytics.service.ts diff --git a/src/Squidex/app/framework/services/clipboard.service.spec.ts b/frontend/app/framework/services/clipboard.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/clipboard.service.spec.ts rename to frontend/app/framework/services/clipboard.service.spec.ts diff --git a/src/Squidex/app/framework/services/clipboard.service.ts b/frontend/app/framework/services/clipboard.service.ts similarity index 100% rename from src/Squidex/app/framework/services/clipboard.service.ts rename to frontend/app/framework/services/clipboard.service.ts diff --git a/src/Squidex/app/framework/services/dialog.service.spec.ts b/frontend/app/framework/services/dialog.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/dialog.service.spec.ts rename to frontend/app/framework/services/dialog.service.spec.ts diff --git a/src/Squidex/app/framework/services/dialog.service.ts b/frontend/app/framework/services/dialog.service.ts similarity index 100% rename from src/Squidex/app/framework/services/dialog.service.ts rename to frontend/app/framework/services/dialog.service.ts diff --git a/src/Squidex/app/framework/services/loading.service.spec.ts b/frontend/app/framework/services/loading.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/loading.service.spec.ts rename to frontend/app/framework/services/loading.service.spec.ts diff --git a/src/Squidex/app/framework/services/loading.service.ts b/frontend/app/framework/services/loading.service.ts similarity index 100% rename from src/Squidex/app/framework/services/loading.service.ts rename to frontend/app/framework/services/loading.service.ts diff --git a/src/Squidex/app/framework/services/local-store.service.spec.ts b/frontend/app/framework/services/local-store.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/local-store.service.spec.ts rename to frontend/app/framework/services/local-store.service.spec.ts diff --git a/src/Squidex/app/framework/services/local-store.service.ts b/frontend/app/framework/services/local-store.service.ts similarity index 100% rename from src/Squidex/app/framework/services/local-store.service.ts rename to frontend/app/framework/services/local-store.service.ts diff --git a/src/Squidex/app/framework/services/message-bus.service.spec.ts b/frontend/app/framework/services/message-bus.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/message-bus.service.spec.ts rename to frontend/app/framework/services/message-bus.service.spec.ts diff --git a/src/Squidex/app/framework/services/message-bus.service.ts b/frontend/app/framework/services/message-bus.service.ts similarity index 100% rename from src/Squidex/app/framework/services/message-bus.service.ts rename to frontend/app/framework/services/message-bus.service.ts diff --git a/src/Squidex/app/framework/services/onboarding.service.spec.ts b/frontend/app/framework/services/onboarding.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/onboarding.service.spec.ts rename to frontend/app/framework/services/onboarding.service.spec.ts diff --git a/src/Squidex/app/framework/services/onboarding.service.ts b/frontend/app/framework/services/onboarding.service.ts similarity index 100% rename from src/Squidex/app/framework/services/onboarding.service.ts rename to frontend/app/framework/services/onboarding.service.ts diff --git a/src/Squidex/app/framework/services/resource-loader.service.ts b/frontend/app/framework/services/resource-loader.service.ts similarity index 100% rename from src/Squidex/app/framework/services/resource-loader.service.ts rename to frontend/app/framework/services/resource-loader.service.ts diff --git a/src/Squidex/app/framework/services/shortcut.service.spec.ts b/frontend/app/framework/services/shortcut.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/shortcut.service.spec.ts rename to frontend/app/framework/services/shortcut.service.spec.ts diff --git a/src/Squidex/app/framework/services/shortcut.service.ts b/frontend/app/framework/services/shortcut.service.ts similarity index 100% rename from src/Squidex/app/framework/services/shortcut.service.ts rename to frontend/app/framework/services/shortcut.service.ts diff --git a/src/Squidex/app/framework/services/title.service.spec.ts b/frontend/app/framework/services/title.service.spec.ts similarity index 100% rename from src/Squidex/app/framework/services/title.service.spec.ts rename to frontend/app/framework/services/title.service.spec.ts diff --git a/src/Squidex/app/framework/services/title.service.ts b/frontend/app/framework/services/title.service.ts similarity index 100% rename from src/Squidex/app/framework/services/title.service.ts rename to frontend/app/framework/services/title.service.ts diff --git a/src/Squidex/app/framework/state.ts b/frontend/app/framework/state.ts similarity index 100% rename from src/Squidex/app/framework/state.ts rename to frontend/app/framework/state.ts diff --git a/src/Squidex/app/framework/utils/array-extensions.spec.ts b/frontend/app/framework/utils/array-extensions.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/array-extensions.spec.ts rename to frontend/app/framework/utils/array-extensions.spec.ts diff --git a/src/Squidex/app/framework/utils/array-extensions.ts b/frontend/app/framework/utils/array-extensions.ts similarity index 100% rename from src/Squidex/app/framework/utils/array-extensions.ts rename to frontend/app/framework/utils/array-extensions.ts diff --git a/src/Squidex/app/framework/utils/array-helper.ts b/frontend/app/framework/utils/array-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/array-helper.ts rename to frontend/app/framework/utils/array-helper.ts diff --git a/src/Squidex/app/framework/utils/date-helper.spec.ts b/frontend/app/framework/utils/date-helper.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-helper.spec.ts rename to frontend/app/framework/utils/date-helper.spec.ts diff --git a/src/Squidex/app/framework/utils/date-helper.ts b/frontend/app/framework/utils/date-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-helper.ts rename to frontend/app/framework/utils/date-helper.ts diff --git a/src/Squidex/app/framework/utils/date-time.spec.ts b/frontend/app/framework/utils/date-time.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-time.spec.ts rename to frontend/app/framework/utils/date-time.spec.ts diff --git a/src/Squidex/app/framework/utils/date-time.ts b/frontend/app/framework/utils/date-time.ts similarity index 100% rename from src/Squidex/app/framework/utils/date-time.ts rename to frontend/app/framework/utils/date-time.ts diff --git a/src/Squidex/app/framework/utils/duration.spec.ts b/frontend/app/framework/utils/duration.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/duration.spec.ts rename to frontend/app/framework/utils/duration.spec.ts diff --git a/src/Squidex/app/framework/utils/duration.ts b/frontend/app/framework/utils/duration.ts similarity index 100% rename from src/Squidex/app/framework/utils/duration.ts rename to frontend/app/framework/utils/duration.ts diff --git a/src/Squidex/app/framework/utils/error.spec.ts b/frontend/app/framework/utils/error.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/error.spec.ts rename to frontend/app/framework/utils/error.spec.ts diff --git a/src/Squidex/app/framework/utils/error.ts b/frontend/app/framework/utils/error.ts similarity index 100% rename from src/Squidex/app/framework/utils/error.ts rename to frontend/app/framework/utils/error.ts diff --git a/src/Squidex/app/framework/utils/hateos.ts b/frontend/app/framework/utils/hateos.ts similarity index 100% rename from src/Squidex/app/framework/utils/hateos.ts rename to frontend/app/framework/utils/hateos.ts diff --git a/src/Squidex/app/framework/utils/interpolator.spec.ts b/frontend/app/framework/utils/interpolator.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/interpolator.spec.ts rename to frontend/app/framework/utils/interpolator.spec.ts diff --git a/src/Squidex/app/framework/utils/interpolator.ts b/frontend/app/framework/utils/interpolator.ts similarity index 100% rename from src/Squidex/app/framework/utils/interpolator.ts rename to frontend/app/framework/utils/interpolator.ts diff --git a/src/Squidex/app/framework/utils/keys.ts b/frontend/app/framework/utils/keys.ts similarity index 100% rename from src/Squidex/app/framework/utils/keys.ts rename to frontend/app/framework/utils/keys.ts diff --git a/src/Squidex/app/framework/utils/math-helper.spec.ts b/frontend/app/framework/utils/math-helper.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/math-helper.spec.ts rename to frontend/app/framework/utils/math-helper.spec.ts diff --git a/src/Squidex/app/framework/utils/math-helper.ts b/frontend/app/framework/utils/math-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/math-helper.ts rename to frontend/app/framework/utils/math-helper.ts diff --git a/src/Squidex/app/framework/utils/modal-positioner.spec.ts b/frontend/app/framework/utils/modal-positioner.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-positioner.spec.ts rename to frontend/app/framework/utils/modal-positioner.spec.ts diff --git a/src/Squidex/app/framework/utils/modal-positioner.ts b/frontend/app/framework/utils/modal-positioner.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-positioner.ts rename to frontend/app/framework/utils/modal-positioner.ts diff --git a/src/Squidex/app/framework/utils/modal-view.spec.ts b/frontend/app/framework/utils/modal-view.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-view.spec.ts rename to frontend/app/framework/utils/modal-view.spec.ts diff --git a/src/Squidex/app/framework/utils/modal-view.ts b/frontend/app/framework/utils/modal-view.ts similarity index 100% rename from src/Squidex/app/framework/utils/modal-view.ts rename to frontend/app/framework/utils/modal-view.ts diff --git a/src/Squidex/app/framework/utils/pager.spec.ts b/frontend/app/framework/utils/pager.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/pager.spec.ts rename to frontend/app/framework/utils/pager.spec.ts diff --git a/src/Squidex/app/framework/utils/pager.ts b/frontend/app/framework/utils/pager.ts similarity index 100% rename from src/Squidex/app/framework/utils/pager.ts rename to frontend/app/framework/utils/pager.ts diff --git a/src/Squidex/app/framework/utils/picasso.ts b/frontend/app/framework/utils/picasso.ts similarity index 100% rename from src/Squidex/app/framework/utils/picasso.ts rename to frontend/app/framework/utils/picasso.ts diff --git a/src/Squidex/app/framework/utils/rxjs-extensions.ts b/frontend/app/framework/utils/rxjs-extensions.ts similarity index 100% rename from src/Squidex/app/framework/utils/rxjs-extensions.ts rename to frontend/app/framework/utils/rxjs-extensions.ts diff --git a/src/Squidex/app/framework/utils/string-helper.spec.ts b/frontend/app/framework/utils/string-helper.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/string-helper.spec.ts rename to frontend/app/framework/utils/string-helper.spec.ts diff --git a/src/Squidex/app/framework/utils/string-helper.ts b/frontend/app/framework/utils/string-helper.ts similarity index 100% rename from src/Squidex/app/framework/utils/string-helper.ts rename to frontend/app/framework/utils/string-helper.ts diff --git a/src/Squidex/app/framework/utils/types.spec.ts b/frontend/app/framework/utils/types.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/types.spec.ts rename to frontend/app/framework/utils/types.spec.ts diff --git a/src/Squidex/app/framework/utils/types.ts b/frontend/app/framework/utils/types.ts similarity index 100% rename from src/Squidex/app/framework/utils/types.ts rename to frontend/app/framework/utils/types.ts diff --git a/src/Squidex/app/framework/utils/version.spec.ts b/frontend/app/framework/utils/version.spec.ts similarity index 100% rename from src/Squidex/app/framework/utils/version.spec.ts rename to frontend/app/framework/utils/version.spec.ts diff --git a/src/Squidex/app/framework/utils/version.ts b/frontend/app/framework/utils/version.ts similarity index 100% rename from src/Squidex/app/framework/utils/version.ts rename to frontend/app/framework/utils/version.ts diff --git a/src/Squidex/wwwroot/index.html b/frontend/app/index.html similarity index 100% rename from src/Squidex/wwwroot/index.html rename to frontend/app/index.html diff --git a/src/Squidex/app/shared/components/app-form.component.html b/frontend/app/shared/components/app-form.component.html similarity index 100% rename from src/Squidex/app/shared/components/app-form.component.html rename to frontend/app/shared/components/app-form.component.html diff --git a/src/Squidex/app/shared/components/app-form.component.scss b/frontend/app/shared/components/app-form.component.scss similarity index 100% rename from src/Squidex/app/shared/components/app-form.component.scss rename to frontend/app/shared/components/app-form.component.scss diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/frontend/app/shared/components/app-form.component.ts similarity index 100% rename from src/Squidex/app/shared/components/app-form.component.ts rename to frontend/app/shared/components/app-form.component.ts diff --git a/src/Squidex/app/shared/components/asset-dialog.component.html b/frontend/app/shared/components/asset-dialog.component.html similarity index 100% rename from src/Squidex/app/shared/components/asset-dialog.component.html rename to frontend/app/shared/components/asset-dialog.component.html diff --git a/src/Squidex/app/shared/components/asset-dialog.component.scss b/frontend/app/shared/components/asset-dialog.component.scss similarity index 100% rename from src/Squidex/app/shared/components/asset-dialog.component.scss rename to frontend/app/shared/components/asset-dialog.component.scss diff --git a/src/Squidex/app/shared/components/asset-dialog.component.ts b/frontend/app/shared/components/asset-dialog.component.ts similarity index 100% rename from src/Squidex/app/shared/components/asset-dialog.component.ts rename to frontend/app/shared/components/asset-dialog.component.ts diff --git a/src/Squidex/app/shared/components/asset-uploader.component.html b/frontend/app/shared/components/asset-uploader.component.html similarity index 100% rename from src/Squidex/app/shared/components/asset-uploader.component.html rename to frontend/app/shared/components/asset-uploader.component.html diff --git a/src/Squidex/app/shared/components/asset-uploader.component.scss b/frontend/app/shared/components/asset-uploader.component.scss similarity index 100% rename from src/Squidex/app/shared/components/asset-uploader.component.scss rename to frontend/app/shared/components/asset-uploader.component.scss diff --git a/src/Squidex/app/shared/components/asset-uploader.component.ts b/frontend/app/shared/components/asset-uploader.component.ts similarity index 100% rename from src/Squidex/app/shared/components/asset-uploader.component.ts rename to frontend/app/shared/components/asset-uploader.component.ts diff --git a/src/Squidex/app/shared/components/asset.component.html b/frontend/app/shared/components/asset.component.html similarity index 100% rename from src/Squidex/app/shared/components/asset.component.html rename to frontend/app/shared/components/asset.component.html diff --git a/src/Squidex/app/shared/components/asset.component.scss b/frontend/app/shared/components/asset.component.scss similarity index 100% rename from src/Squidex/app/shared/components/asset.component.scss rename to frontend/app/shared/components/asset.component.scss diff --git a/src/Squidex/app/shared/components/asset.component.ts b/frontend/app/shared/components/asset.component.ts similarity index 100% rename from src/Squidex/app/shared/components/asset.component.ts rename to frontend/app/shared/components/asset.component.ts diff --git a/src/Squidex/app/shared/components/assets-list.component.html b/frontend/app/shared/components/assets-list.component.html similarity index 100% rename from src/Squidex/app/shared/components/assets-list.component.html rename to frontend/app/shared/components/assets-list.component.html diff --git a/src/Squidex/app/shared/components/assets-list.component.scss b/frontend/app/shared/components/assets-list.component.scss similarity index 100% rename from src/Squidex/app/shared/components/assets-list.component.scss rename to frontend/app/shared/components/assets-list.component.scss diff --git a/src/Squidex/app/shared/components/assets-list.component.ts b/frontend/app/shared/components/assets-list.component.ts similarity index 100% rename from src/Squidex/app/shared/components/assets-list.component.ts rename to frontend/app/shared/components/assets-list.component.ts diff --git a/src/Squidex/app/shared/components/assets-selector.component.html b/frontend/app/shared/components/assets-selector.component.html similarity index 100% rename from src/Squidex/app/shared/components/assets-selector.component.html rename to frontend/app/shared/components/assets-selector.component.html diff --git a/src/Squidex/app/shared/components/assets-selector.component.scss b/frontend/app/shared/components/assets-selector.component.scss similarity index 100% rename from src/Squidex/app/shared/components/assets-selector.component.scss rename to frontend/app/shared/components/assets-selector.component.scss diff --git a/src/Squidex/app/shared/components/assets-selector.component.ts b/frontend/app/shared/components/assets-selector.component.ts similarity index 100% rename from src/Squidex/app/shared/components/assets-selector.component.ts rename to frontend/app/shared/components/assets-selector.component.ts diff --git a/src/Squidex/app/shared/components/comment.component.html b/frontend/app/shared/components/comment.component.html similarity index 100% rename from src/Squidex/app/shared/components/comment.component.html rename to frontend/app/shared/components/comment.component.html diff --git a/src/Squidex/app/shared/components/comment.component.scss b/frontend/app/shared/components/comment.component.scss similarity index 100% rename from src/Squidex/app/shared/components/comment.component.scss rename to frontend/app/shared/components/comment.component.scss diff --git a/src/Squidex/app/shared/components/comment.component.ts b/frontend/app/shared/components/comment.component.ts similarity index 100% rename from src/Squidex/app/shared/components/comment.component.ts rename to frontend/app/shared/components/comment.component.ts diff --git a/src/Squidex/app/shared/components/comments.component.html b/frontend/app/shared/components/comments.component.html similarity index 100% rename from src/Squidex/app/shared/components/comments.component.html rename to frontend/app/shared/components/comments.component.html diff --git a/src/Squidex/app/shared/components/comments.component.scss b/frontend/app/shared/components/comments.component.scss similarity index 100% rename from src/Squidex/app/shared/components/comments.component.scss rename to frontend/app/shared/components/comments.component.scss diff --git a/src/Squidex/app/shared/components/comments.component.ts b/frontend/app/shared/components/comments.component.ts similarity index 100% rename from src/Squidex/app/shared/components/comments.component.ts rename to frontend/app/shared/components/comments.component.ts diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.html b/frontend/app/shared/components/geolocation-editor.component.html similarity index 100% rename from src/Squidex/app/shared/components/geolocation-editor.component.html rename to frontend/app/shared/components/geolocation-editor.component.html diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.scss b/frontend/app/shared/components/geolocation-editor.component.scss similarity index 100% rename from src/Squidex/app/shared/components/geolocation-editor.component.scss rename to frontend/app/shared/components/geolocation-editor.component.scss diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/frontend/app/shared/components/geolocation-editor.component.ts similarity index 100% rename from src/Squidex/app/shared/components/geolocation-editor.component.ts rename to frontend/app/shared/components/geolocation-editor.component.ts diff --git a/src/Squidex/app/shared/components/help-markdown.pipe.spec.ts b/frontend/app/shared/components/help-markdown.pipe.spec.ts similarity index 100% rename from src/Squidex/app/shared/components/help-markdown.pipe.spec.ts rename to frontend/app/shared/components/help-markdown.pipe.spec.ts diff --git a/src/Squidex/app/shared/components/help-markdown.pipe.ts b/frontend/app/shared/components/help-markdown.pipe.ts similarity index 100% rename from src/Squidex/app/shared/components/help-markdown.pipe.ts rename to frontend/app/shared/components/help-markdown.pipe.ts diff --git a/src/Squidex/app/shared/components/help.component.html b/frontend/app/shared/components/help.component.html similarity index 100% rename from src/Squidex/app/shared/components/help.component.html rename to frontend/app/shared/components/help.component.html diff --git a/src/Squidex/app/shared/components/help.component.scss b/frontend/app/shared/components/help.component.scss similarity index 100% rename from src/Squidex/app/shared/components/help.component.scss rename to frontend/app/shared/components/help.component.scss diff --git a/src/Squidex/app/shared/components/help.component.ts b/frontend/app/shared/components/help.component.ts similarity index 100% rename from src/Squidex/app/shared/components/help.component.ts rename to frontend/app/shared/components/help.component.ts diff --git a/src/Squidex/app/shared/components/history-list.component.html b/frontend/app/shared/components/history-list.component.html similarity index 100% rename from src/Squidex/app/shared/components/history-list.component.html rename to frontend/app/shared/components/history-list.component.html diff --git a/src/Squidex/app/shared/components/history-list.component.scss b/frontend/app/shared/components/history-list.component.scss similarity index 100% rename from src/Squidex/app/shared/components/history-list.component.scss rename to frontend/app/shared/components/history-list.component.scss diff --git a/src/Squidex/app/shared/components/history-list.component.ts b/frontend/app/shared/components/history-list.component.ts similarity index 100% rename from src/Squidex/app/shared/components/history-list.component.ts rename to frontend/app/shared/components/history-list.component.ts diff --git a/src/Squidex/app/shared/components/history.component.html b/frontend/app/shared/components/history.component.html similarity index 100% rename from src/Squidex/app/shared/components/history.component.html rename to frontend/app/shared/components/history.component.html diff --git a/src/Squidex/app/shared/components/history.component.scss b/frontend/app/shared/components/history.component.scss similarity index 100% rename from src/Squidex/app/shared/components/history.component.scss rename to frontend/app/shared/components/history.component.scss diff --git a/src/Squidex/app/shared/components/history.component.ts b/frontend/app/shared/components/history.component.ts similarity index 100% rename from src/Squidex/app/shared/components/history.component.ts rename to frontend/app/shared/components/history.component.ts diff --git a/src/Squidex/app/shared/components/language-selector.component.html b/frontend/app/shared/components/language-selector.component.html similarity index 100% rename from src/Squidex/app/shared/components/language-selector.component.html rename to frontend/app/shared/components/language-selector.component.html diff --git a/src/Squidex/app/shared/components/language-selector.component.scss b/frontend/app/shared/components/language-selector.component.scss similarity index 100% rename from src/Squidex/app/shared/components/language-selector.component.scss rename to frontend/app/shared/components/language-selector.component.scss diff --git a/src/Squidex/app/shared/components/language-selector.component.ts b/frontend/app/shared/components/language-selector.component.ts similarity index 100% rename from src/Squidex/app/shared/components/language-selector.component.ts rename to frontend/app/shared/components/language-selector.component.ts diff --git a/src/Squidex/app/shared/components/markdown-editor.component.html b/frontend/app/shared/components/markdown-editor.component.html similarity index 100% rename from src/Squidex/app/shared/components/markdown-editor.component.html rename to frontend/app/shared/components/markdown-editor.component.html diff --git a/src/Squidex/app/shared/components/markdown-editor.component.scss b/frontend/app/shared/components/markdown-editor.component.scss similarity index 100% rename from src/Squidex/app/shared/components/markdown-editor.component.scss rename to frontend/app/shared/components/markdown-editor.component.scss diff --git a/src/Squidex/app/shared/components/markdown-editor.component.ts b/frontend/app/shared/components/markdown-editor.component.ts similarity index 100% rename from src/Squidex/app/shared/components/markdown-editor.component.ts rename to frontend/app/shared/components/markdown-editor.component.ts diff --git a/src/Squidex/app/shared/components/pipes.ts b/frontend/app/shared/components/pipes.ts similarity index 100% rename from src/Squidex/app/shared/components/pipes.ts rename to frontend/app/shared/components/pipes.ts diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.html b/frontend/app/shared/components/queries/filter-comparison.component.html similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-comparison.component.html rename to frontend/app/shared/components/queries/filter-comparison.component.html diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.scss b/frontend/app/shared/components/queries/filter-comparison.component.scss similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-comparison.component.scss rename to frontend/app/shared/components/queries/filter-comparison.component.scss diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.ts b/frontend/app/shared/components/queries/filter-comparison.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-comparison.component.ts rename to frontend/app/shared/components/queries/filter-comparison.component.ts diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.html b/frontend/app/shared/components/queries/filter-logical.component.html similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-logical.component.html rename to frontend/app/shared/components/queries/filter-logical.component.html diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.scss b/frontend/app/shared/components/queries/filter-logical.component.scss similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-logical.component.scss rename to frontend/app/shared/components/queries/filter-logical.component.scss diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.ts b/frontend/app/shared/components/queries/filter-logical.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-logical.component.ts rename to frontend/app/shared/components/queries/filter-logical.component.ts diff --git a/src/Squidex/app/shared/components/queries/filter-node.component.ts b/frontend/app/shared/components/queries/filter-node.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/filter-node.component.ts rename to frontend/app/shared/components/queries/filter-node.component.ts diff --git a/src/Squidex/app/shared/components/queries/query.component.ts b/frontend/app/shared/components/queries/query.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/query.component.ts rename to frontend/app/shared/components/queries/query.component.ts diff --git a/src/Squidex/app/shared/components/queries/sorting.component.ts b/frontend/app/shared/components/queries/sorting.component.ts similarity index 100% rename from src/Squidex/app/shared/components/queries/sorting.component.ts rename to frontend/app/shared/components/queries/sorting.component.ts diff --git a/src/Squidex/app/shared/components/references-dropdown.component.ts b/frontend/app/shared/components/references-dropdown.component.ts similarity index 100% rename from src/Squidex/app/shared/components/references-dropdown.component.ts rename to frontend/app/shared/components/references-dropdown.component.ts diff --git a/src/Squidex/app/shared/components/rich-editor.component.html b/frontend/app/shared/components/rich-editor.component.html similarity index 100% rename from src/Squidex/app/shared/components/rich-editor.component.html rename to frontend/app/shared/components/rich-editor.component.html diff --git a/src/Squidex/app/shared/components/rich-editor.component.scss b/frontend/app/shared/components/rich-editor.component.scss similarity index 100% rename from src/Squidex/app/shared/components/rich-editor.component.scss rename to frontend/app/shared/components/rich-editor.component.scss diff --git a/src/Squidex/app/shared/components/rich-editor.component.ts b/frontend/app/shared/components/rich-editor.component.ts similarity index 100% rename from src/Squidex/app/shared/components/rich-editor.component.ts rename to frontend/app/shared/components/rich-editor.component.ts diff --git a/src/Squidex/app/shared/components/saved-queries.component.ts b/frontend/app/shared/components/saved-queries.component.ts similarity index 100% rename from src/Squidex/app/shared/components/saved-queries.component.ts rename to frontend/app/shared/components/saved-queries.component.ts diff --git a/src/Squidex/app/shared/components/schema-category.component.html b/frontend/app/shared/components/schema-category.component.html similarity index 100% rename from src/Squidex/app/shared/components/schema-category.component.html rename to frontend/app/shared/components/schema-category.component.html diff --git a/src/Squidex/app/shared/components/schema-category.component.scss b/frontend/app/shared/components/schema-category.component.scss similarity index 100% rename from src/Squidex/app/shared/components/schema-category.component.scss rename to frontend/app/shared/components/schema-category.component.scss diff --git a/src/Squidex/app/shared/components/schema-category.component.ts b/frontend/app/shared/components/schema-category.component.ts similarity index 100% rename from src/Squidex/app/shared/components/schema-category.component.ts rename to frontend/app/shared/components/schema-category.component.ts diff --git a/src/Squidex/app/shared/components/search-form.component.html b/frontend/app/shared/components/search-form.component.html similarity index 100% rename from src/Squidex/app/shared/components/search-form.component.html rename to frontend/app/shared/components/search-form.component.html diff --git a/src/Squidex/app/shared/components/search-form.component.scss b/frontend/app/shared/components/search-form.component.scss similarity index 100% rename from src/Squidex/app/shared/components/search-form.component.scss rename to frontend/app/shared/components/search-form.component.scss diff --git a/src/Squidex/app/shared/components/search-form.component.ts b/frontend/app/shared/components/search-form.component.ts similarity index 100% rename from src/Squidex/app/shared/components/search-form.component.ts rename to frontend/app/shared/components/search-form.component.ts diff --git a/src/Squidex/app/shared/components/table-header.component.ts b/frontend/app/shared/components/table-header.component.ts similarity index 100% rename from src/Squidex/app/shared/components/table-header.component.ts rename to frontend/app/shared/components/table-header.component.ts diff --git a/src/Squidex/app/shared/declarations.ts b/frontend/app/shared/declarations.ts similarity index 100% rename from src/Squidex/app/shared/declarations.ts rename to frontend/app/shared/declarations.ts diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts b/frontend/app/shared/guards/app-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts rename to frontend/app/shared/guards/app-must-exist.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.ts b/frontend/app/shared/guards/app-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/app-must-exist.guard.ts rename to frontend/app/shared/guards/app-must-exist.guard.ts diff --git a/src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts b/frontend/app/shared/guards/content-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts rename to frontend/app/shared/guards/content-must-exist.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/content-must-exist.guard.ts b/frontend/app/shared/guards/content-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/content-must-exist.guard.ts rename to frontend/app/shared/guards/content-must-exist.guard.ts diff --git a/src/Squidex/app/shared/guards/load-apps.guard.spec.ts b/frontend/app/shared/guards/load-apps.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-apps.guard.spec.ts rename to frontend/app/shared/guards/load-apps.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/load-apps.guard.ts b/frontend/app/shared/guards/load-apps.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-apps.guard.ts rename to frontend/app/shared/guards/load-apps.guard.ts diff --git a/src/Squidex/app/shared/guards/load-languages.guard.spec.ts b/frontend/app/shared/guards/load-languages.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-languages.guard.spec.ts rename to frontend/app/shared/guards/load-languages.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/load-languages.guard.ts b/frontend/app/shared/guards/load-languages.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/load-languages.guard.ts rename to frontend/app/shared/guards/load-languages.guard.ts diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts b/frontend/app/shared/guards/must-be-authenticated.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts rename to frontend/app/shared/guards/must-be-authenticated.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/must-be-authenticated.guard.ts b/frontend/app/shared/guards/must-be-authenticated.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-authenticated.guard.ts rename to frontend/app/shared/guards/must-be-authenticated.guard.ts diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts b/frontend/app/shared/guards/must-be-not-authenticated.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts rename to frontend/app/shared/guards/must-be-not-authenticated.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts b/frontend/app/shared/guards/must-be-not-authenticated.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts rename to frontend/app/shared/guards/must-be-not-authenticated.guard.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts b/frontend/app/shared/guards/schema-must-exist-published.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts rename to frontend/app/shared/guards/schema-must-exist-published.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts b/frontend/app/shared/guards/schema-must-exist-published.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts rename to frontend/app/shared/guards/schema-must-exist-published.guard.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts b/frontend/app/shared/guards/schema-must-exist.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts rename to frontend/app/shared/guards/schema-must-exist.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/schema-must-exist.guard.ts b/frontend/app/shared/guards/schema-must-exist.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-exist.guard.ts rename to frontend/app/shared/guards/schema-must-exist.guard.ts diff --git a/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts b/frontend/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts rename to frontend/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts b/frontend/app/shared/guards/schema-must-not-be-singleton.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts rename to frontend/app/shared/guards/schema-must-not-be-singleton.guard.ts diff --git a/src/Squidex/app/shared/guards/unset-app.guard.spec.ts b/frontend/app/shared/guards/unset-app.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-app.guard.spec.ts rename to frontend/app/shared/guards/unset-app.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/unset-app.guard.ts b/frontend/app/shared/guards/unset-app.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-app.guard.ts rename to frontend/app/shared/guards/unset-app.guard.ts diff --git a/src/Squidex/app/shared/guards/unset-content.guard.spec.ts b/frontend/app/shared/guards/unset-content.guard.spec.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-content.guard.spec.ts rename to frontend/app/shared/guards/unset-content.guard.spec.ts diff --git a/src/Squidex/app/shared/guards/unset-content.guard.ts b/frontend/app/shared/guards/unset-content.guard.ts similarity index 100% rename from src/Squidex/app/shared/guards/unset-content.guard.ts rename to frontend/app/shared/guards/unset-content.guard.ts diff --git a/src/Squidex/app/shared/index.ts b/frontend/app/shared/index.ts similarity index 100% rename from src/Squidex/app/shared/index.ts rename to frontend/app/shared/index.ts diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts b/frontend/app/shared/interceptors/auth.interceptor.spec.ts similarity index 100% rename from src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts rename to frontend/app/shared/interceptors/auth.interceptor.spec.ts diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.ts b/frontend/app/shared/interceptors/auth.interceptor.ts similarity index 100% rename from src/Squidex/app/shared/interceptors/auth.interceptor.ts rename to frontend/app/shared/interceptors/auth.interceptor.ts diff --git a/src/Squidex/app/shared/internal.ts b/frontend/app/shared/internal.ts similarity index 100% rename from src/Squidex/app/shared/internal.ts rename to frontend/app/shared/internal.ts diff --git a/src/Squidex/app/shared/module.ts b/frontend/app/shared/module.ts similarity index 100% rename from src/Squidex/app/shared/module.ts rename to frontend/app/shared/module.ts diff --git a/src/Squidex/app/shared/services/app-languages.service.spec.ts b/frontend/app/shared/services/app-languages.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/app-languages.service.spec.ts rename to frontend/app/shared/services/app-languages.service.spec.ts diff --git a/src/Squidex/app/shared/services/app-languages.service.ts b/frontend/app/shared/services/app-languages.service.ts similarity index 100% rename from src/Squidex/app/shared/services/app-languages.service.ts rename to frontend/app/shared/services/app-languages.service.ts diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/frontend/app/shared/services/apps.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/apps.service.spec.ts rename to frontend/app/shared/services/apps.service.spec.ts diff --git a/src/Squidex/app/shared/services/apps.service.ts b/frontend/app/shared/services/apps.service.ts similarity index 100% rename from src/Squidex/app/shared/services/apps.service.ts rename to frontend/app/shared/services/apps.service.ts diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/frontend/app/shared/services/assets.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/assets.service.spec.ts rename to frontend/app/shared/services/assets.service.spec.ts diff --git a/src/Squidex/app/shared/services/assets.service.ts b/frontend/app/shared/services/assets.service.ts similarity index 100% rename from src/Squidex/app/shared/services/assets.service.ts rename to frontend/app/shared/services/assets.service.ts diff --git a/src/Squidex/app/shared/services/auth.service.ts b/frontend/app/shared/services/auth.service.ts similarity index 100% rename from src/Squidex/app/shared/services/auth.service.ts rename to frontend/app/shared/services/auth.service.ts diff --git a/src/Squidex/app/shared/services/autosave.service.spec.ts b/frontend/app/shared/services/autosave.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/autosave.service.spec.ts rename to frontend/app/shared/services/autosave.service.spec.ts diff --git a/src/Squidex/app/shared/services/autosave.service.ts b/frontend/app/shared/services/autosave.service.ts similarity index 100% rename from src/Squidex/app/shared/services/autosave.service.ts rename to frontend/app/shared/services/autosave.service.ts diff --git a/src/Squidex/app/shared/services/backups.service.spec.ts b/frontend/app/shared/services/backups.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/backups.service.spec.ts rename to frontend/app/shared/services/backups.service.spec.ts diff --git a/src/Squidex/app/shared/services/backups.service.ts b/frontend/app/shared/services/backups.service.ts similarity index 100% rename from src/Squidex/app/shared/services/backups.service.ts rename to frontend/app/shared/services/backups.service.ts diff --git a/src/Squidex/app/shared/services/clients.service.spec.ts b/frontend/app/shared/services/clients.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/clients.service.spec.ts rename to frontend/app/shared/services/clients.service.spec.ts diff --git a/src/Squidex/app/shared/services/clients.service.ts b/frontend/app/shared/services/clients.service.ts similarity index 100% rename from src/Squidex/app/shared/services/clients.service.ts rename to frontend/app/shared/services/clients.service.ts diff --git a/src/Squidex/app/shared/services/comments.service.spec.ts b/frontend/app/shared/services/comments.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/comments.service.spec.ts rename to frontend/app/shared/services/comments.service.spec.ts diff --git a/src/Squidex/app/shared/services/comments.service.ts b/frontend/app/shared/services/comments.service.ts similarity index 100% rename from src/Squidex/app/shared/services/comments.service.ts rename to frontend/app/shared/services/comments.service.ts diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/frontend/app/shared/services/contents.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/contents.service.spec.ts rename to frontend/app/shared/services/contents.service.spec.ts diff --git a/src/Squidex/app/shared/services/contents.service.ts b/frontend/app/shared/services/contents.service.ts similarity index 100% rename from src/Squidex/app/shared/services/contents.service.ts rename to frontend/app/shared/services/contents.service.ts diff --git a/src/Squidex/app/shared/services/contributors.service.spec.ts b/frontend/app/shared/services/contributors.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/contributors.service.spec.ts rename to frontend/app/shared/services/contributors.service.spec.ts diff --git a/src/Squidex/app/shared/services/contributors.service.ts b/frontend/app/shared/services/contributors.service.ts similarity index 100% rename from src/Squidex/app/shared/services/contributors.service.ts rename to frontend/app/shared/services/contributors.service.ts diff --git a/src/Squidex/app/shared/services/graphql.service.spec.ts b/frontend/app/shared/services/graphql.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/graphql.service.spec.ts rename to frontend/app/shared/services/graphql.service.spec.ts diff --git a/src/Squidex/app/shared/services/graphql.service.ts b/frontend/app/shared/services/graphql.service.ts similarity index 100% rename from src/Squidex/app/shared/services/graphql.service.ts rename to frontend/app/shared/services/graphql.service.ts diff --git a/src/Squidex/app/shared/services/help.service.spec.ts b/frontend/app/shared/services/help.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/help.service.spec.ts rename to frontend/app/shared/services/help.service.spec.ts diff --git a/src/Squidex/app/shared/services/help.service.ts b/frontend/app/shared/services/help.service.ts similarity index 100% rename from src/Squidex/app/shared/services/help.service.ts rename to frontend/app/shared/services/help.service.ts diff --git a/src/Squidex/app/shared/services/history.service.spec.ts b/frontend/app/shared/services/history.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/history.service.spec.ts rename to frontend/app/shared/services/history.service.spec.ts diff --git a/src/Squidex/app/shared/services/history.service.ts b/frontend/app/shared/services/history.service.ts similarity index 100% rename from src/Squidex/app/shared/services/history.service.ts rename to frontend/app/shared/services/history.service.ts diff --git a/src/Squidex/app/shared/services/languages.service.spec.ts b/frontend/app/shared/services/languages.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/languages.service.spec.ts rename to frontend/app/shared/services/languages.service.spec.ts diff --git a/src/Squidex/app/shared/services/languages.service.ts b/frontend/app/shared/services/languages.service.ts similarity index 100% rename from src/Squidex/app/shared/services/languages.service.ts rename to frontend/app/shared/services/languages.service.ts diff --git a/src/Squidex/app/shared/services/news.service.spec.ts b/frontend/app/shared/services/news.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/news.service.spec.ts rename to frontend/app/shared/services/news.service.spec.ts diff --git a/src/Squidex/app/shared/services/news.service.ts b/frontend/app/shared/services/news.service.ts similarity index 100% rename from src/Squidex/app/shared/services/news.service.ts rename to frontend/app/shared/services/news.service.ts diff --git a/src/Squidex/app/shared/services/patterns.service.spec.ts b/frontend/app/shared/services/patterns.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/patterns.service.spec.ts rename to frontend/app/shared/services/patterns.service.spec.ts diff --git a/src/Squidex/app/shared/services/patterns.service.ts b/frontend/app/shared/services/patterns.service.ts similarity index 100% rename from src/Squidex/app/shared/services/patterns.service.ts rename to frontend/app/shared/services/patterns.service.ts diff --git a/src/Squidex/app/shared/services/plans.service.spec.ts b/frontend/app/shared/services/plans.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/plans.service.spec.ts rename to frontend/app/shared/services/plans.service.spec.ts diff --git a/src/Squidex/app/shared/services/plans.service.ts b/frontend/app/shared/services/plans.service.ts similarity index 100% rename from src/Squidex/app/shared/services/plans.service.ts rename to frontend/app/shared/services/plans.service.ts diff --git a/src/Squidex/app/shared/services/roles.service.spec.ts b/frontend/app/shared/services/roles.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/roles.service.spec.ts rename to frontend/app/shared/services/roles.service.spec.ts diff --git a/src/Squidex/app/shared/services/roles.service.ts b/frontend/app/shared/services/roles.service.ts similarity index 100% rename from src/Squidex/app/shared/services/roles.service.ts rename to frontend/app/shared/services/roles.service.ts diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/frontend/app/shared/services/rules.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/rules.service.spec.ts rename to frontend/app/shared/services/rules.service.spec.ts diff --git a/src/Squidex/app/shared/services/rules.service.ts b/frontend/app/shared/services/rules.service.ts similarity index 100% rename from src/Squidex/app/shared/services/rules.service.ts rename to frontend/app/shared/services/rules.service.ts diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/frontend/app/shared/services/schemas.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/schemas.service.spec.ts rename to frontend/app/shared/services/schemas.service.spec.ts diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts similarity index 100% rename from src/Squidex/app/shared/services/schemas.service.ts rename to frontend/app/shared/services/schemas.service.ts diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts similarity index 100% rename from src/Squidex/app/shared/services/schemas.types.ts rename to frontend/app/shared/services/schemas.types.ts diff --git a/src/Squidex/app/shared/services/translations.service.spec.ts b/frontend/app/shared/services/translations.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/translations.service.spec.ts rename to frontend/app/shared/services/translations.service.spec.ts diff --git a/src/Squidex/app/shared/services/translations.service.ts b/frontend/app/shared/services/translations.service.ts similarity index 100% rename from src/Squidex/app/shared/services/translations.service.ts rename to frontend/app/shared/services/translations.service.ts diff --git a/src/Squidex/app/shared/services/ui.service.spec.ts b/frontend/app/shared/services/ui.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/ui.service.spec.ts rename to frontend/app/shared/services/ui.service.spec.ts diff --git a/src/Squidex/app/shared/services/ui.service.ts b/frontend/app/shared/services/ui.service.ts similarity index 100% rename from src/Squidex/app/shared/services/ui.service.ts rename to frontend/app/shared/services/ui.service.ts diff --git a/src/Squidex/app/shared/services/usages.service.spec.ts b/frontend/app/shared/services/usages.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/usages.service.spec.ts rename to frontend/app/shared/services/usages.service.spec.ts diff --git a/src/Squidex/app/shared/services/usages.service.ts b/frontend/app/shared/services/usages.service.ts similarity index 100% rename from src/Squidex/app/shared/services/usages.service.ts rename to frontend/app/shared/services/usages.service.ts diff --git a/src/Squidex/app/shared/services/users-provider.service.spec.ts b/frontend/app/shared/services/users-provider.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/users-provider.service.spec.ts rename to frontend/app/shared/services/users-provider.service.spec.ts diff --git a/src/Squidex/app/shared/services/users-provider.service.ts b/frontend/app/shared/services/users-provider.service.ts similarity index 100% rename from src/Squidex/app/shared/services/users-provider.service.ts rename to frontend/app/shared/services/users-provider.service.ts diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/frontend/app/shared/services/users.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/users.service.spec.ts rename to frontend/app/shared/services/users.service.spec.ts diff --git a/src/Squidex/app/shared/services/users.service.ts b/frontend/app/shared/services/users.service.ts similarity index 100% rename from src/Squidex/app/shared/services/users.service.ts rename to frontend/app/shared/services/users.service.ts diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/frontend/app/shared/services/workflows.service.spec.ts similarity index 100% rename from src/Squidex/app/shared/services/workflows.service.spec.ts rename to frontend/app/shared/services/workflows.service.spec.ts diff --git a/src/Squidex/app/shared/services/workflows.service.ts b/frontend/app/shared/services/workflows.service.ts similarity index 100% rename from src/Squidex/app/shared/services/workflows.service.ts rename to frontend/app/shared/services/workflows.service.ts diff --git a/src/Squidex/app/shared/state/_test-helpers.ts b/frontend/app/shared/state/_test-helpers.ts similarity index 100% rename from src/Squidex/app/shared/state/_test-helpers.ts rename to frontend/app/shared/state/_test-helpers.ts diff --git a/src/Squidex/app/shared/state/apps.forms.ts b/frontend/app/shared/state/apps.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/apps.forms.ts rename to frontend/app/shared/state/apps.forms.ts diff --git a/src/Squidex/app/shared/state/apps.state.spec.ts b/frontend/app/shared/state/apps.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/apps.state.spec.ts rename to frontend/app/shared/state/apps.state.spec.ts diff --git a/src/Squidex/app/shared/state/apps.state.ts b/frontend/app/shared/state/apps.state.ts similarity index 100% rename from src/Squidex/app/shared/state/apps.state.ts rename to frontend/app/shared/state/apps.state.ts diff --git a/src/Squidex/app/shared/state/asset-uploader.state.spec.ts b/frontend/app/shared/state/asset-uploader.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/asset-uploader.state.spec.ts rename to frontend/app/shared/state/asset-uploader.state.spec.ts diff --git a/src/Squidex/app/shared/state/asset-uploader.state.ts b/frontend/app/shared/state/asset-uploader.state.ts similarity index 100% rename from src/Squidex/app/shared/state/asset-uploader.state.ts rename to frontend/app/shared/state/asset-uploader.state.ts diff --git a/src/Squidex/app/shared/state/assets.forms.ts b/frontend/app/shared/state/assets.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/assets.forms.ts rename to frontend/app/shared/state/assets.forms.ts diff --git a/src/Squidex/app/shared/state/assets.state.spec.ts b/frontend/app/shared/state/assets.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/assets.state.spec.ts rename to frontend/app/shared/state/assets.state.spec.ts diff --git a/src/Squidex/app/shared/state/assets.state.ts b/frontend/app/shared/state/assets.state.ts similarity index 100% rename from src/Squidex/app/shared/state/assets.state.ts rename to frontend/app/shared/state/assets.state.ts diff --git a/src/Squidex/app/shared/state/backups.forms.ts b/frontend/app/shared/state/backups.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/backups.forms.ts rename to frontend/app/shared/state/backups.forms.ts diff --git a/src/Squidex/app/shared/state/backups.state.spec.ts b/frontend/app/shared/state/backups.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/backups.state.spec.ts rename to frontend/app/shared/state/backups.state.spec.ts diff --git a/src/Squidex/app/shared/state/backups.state.ts b/frontend/app/shared/state/backups.state.ts similarity index 100% rename from src/Squidex/app/shared/state/backups.state.ts rename to frontend/app/shared/state/backups.state.ts diff --git a/src/Squidex/app/shared/state/clients.forms.ts b/frontend/app/shared/state/clients.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/clients.forms.ts rename to frontend/app/shared/state/clients.forms.ts diff --git a/src/Squidex/app/shared/state/clients.state.spec.ts b/frontend/app/shared/state/clients.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/clients.state.spec.ts rename to frontend/app/shared/state/clients.state.spec.ts diff --git a/src/Squidex/app/shared/state/clients.state.ts b/frontend/app/shared/state/clients.state.ts similarity index 100% rename from src/Squidex/app/shared/state/clients.state.ts rename to frontend/app/shared/state/clients.state.ts diff --git a/src/Squidex/app/shared/state/comments.form.ts b/frontend/app/shared/state/comments.form.ts similarity index 100% rename from src/Squidex/app/shared/state/comments.form.ts rename to frontend/app/shared/state/comments.form.ts diff --git a/src/Squidex/app/shared/state/comments.state.spec.ts b/frontend/app/shared/state/comments.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/comments.state.spec.ts rename to frontend/app/shared/state/comments.state.spec.ts diff --git a/src/Squidex/app/shared/state/comments.state.ts b/frontend/app/shared/state/comments.state.ts similarity index 100% rename from src/Squidex/app/shared/state/comments.state.ts rename to frontend/app/shared/state/comments.state.ts diff --git a/src/Squidex/app/shared/state/contents.forms.spec.ts b/frontend/app/shared/state/contents.forms.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/contents.forms.spec.ts rename to frontend/app/shared/state/contents.forms.spec.ts diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/contents.forms.ts rename to frontend/app/shared/state/contents.forms.ts diff --git a/src/Squidex/app/shared/state/contents.state.ts b/frontend/app/shared/state/contents.state.ts similarity index 100% rename from src/Squidex/app/shared/state/contents.state.ts rename to frontend/app/shared/state/contents.state.ts diff --git a/src/Squidex/app/shared/state/contributors.forms.ts b/frontend/app/shared/state/contributors.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/contributors.forms.ts rename to frontend/app/shared/state/contributors.forms.ts diff --git a/src/Squidex/app/shared/state/contributors.state.spec.ts b/frontend/app/shared/state/contributors.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/contributors.state.spec.ts rename to frontend/app/shared/state/contributors.state.spec.ts diff --git a/src/Squidex/app/shared/state/contributors.state.ts b/frontend/app/shared/state/contributors.state.ts similarity index 100% rename from src/Squidex/app/shared/state/contributors.state.ts rename to frontend/app/shared/state/contributors.state.ts diff --git a/src/Squidex/app/shared/state/languages.forms.ts b/frontend/app/shared/state/languages.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/languages.forms.ts rename to frontend/app/shared/state/languages.forms.ts diff --git a/src/Squidex/app/shared/state/languages.state.spec.ts b/frontend/app/shared/state/languages.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/languages.state.spec.ts rename to frontend/app/shared/state/languages.state.spec.ts diff --git a/src/Squidex/app/shared/state/languages.state.ts b/frontend/app/shared/state/languages.state.ts similarity index 100% rename from src/Squidex/app/shared/state/languages.state.ts rename to frontend/app/shared/state/languages.state.ts diff --git a/src/Squidex/app/shared/state/patterns.forms.ts b/frontend/app/shared/state/patterns.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/patterns.forms.ts rename to frontend/app/shared/state/patterns.forms.ts diff --git a/src/Squidex/app/shared/state/patterns.state.spec.ts b/frontend/app/shared/state/patterns.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/patterns.state.spec.ts rename to frontend/app/shared/state/patterns.state.spec.ts diff --git a/src/Squidex/app/shared/state/patterns.state.ts b/frontend/app/shared/state/patterns.state.ts similarity index 100% rename from src/Squidex/app/shared/state/patterns.state.ts rename to frontend/app/shared/state/patterns.state.ts diff --git a/src/Squidex/app/shared/state/plans.state.spec.ts b/frontend/app/shared/state/plans.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/plans.state.spec.ts rename to frontend/app/shared/state/plans.state.spec.ts diff --git a/src/Squidex/app/shared/state/plans.state.ts b/frontend/app/shared/state/plans.state.ts similarity index 100% rename from src/Squidex/app/shared/state/plans.state.ts rename to frontend/app/shared/state/plans.state.ts diff --git a/src/Squidex/app/shared/state/queries.spec.ts b/frontend/app/shared/state/queries.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/queries.spec.ts rename to frontend/app/shared/state/queries.spec.ts diff --git a/src/Squidex/app/shared/state/queries.ts b/frontend/app/shared/state/queries.ts similarity index 100% rename from src/Squidex/app/shared/state/queries.ts rename to frontend/app/shared/state/queries.ts diff --git a/src/Squidex/app/shared/state/query.ts b/frontend/app/shared/state/query.ts similarity index 100% rename from src/Squidex/app/shared/state/query.ts rename to frontend/app/shared/state/query.ts diff --git a/src/Squidex/app/shared/state/roles.forms.ts b/frontend/app/shared/state/roles.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/roles.forms.ts rename to frontend/app/shared/state/roles.forms.ts diff --git a/src/Squidex/app/shared/state/roles.state.spec.ts b/frontend/app/shared/state/roles.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/roles.state.spec.ts rename to frontend/app/shared/state/roles.state.spec.ts diff --git a/src/Squidex/app/shared/state/roles.state.ts b/frontend/app/shared/state/roles.state.ts similarity index 100% rename from src/Squidex/app/shared/state/roles.state.ts rename to frontend/app/shared/state/roles.state.ts diff --git a/src/Squidex/app/shared/state/rule-events.state.spec.ts b/frontend/app/shared/state/rule-events.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/rule-events.state.spec.ts rename to frontend/app/shared/state/rule-events.state.spec.ts diff --git a/src/Squidex/app/shared/state/rule-events.state.ts b/frontend/app/shared/state/rule-events.state.ts similarity index 100% rename from src/Squidex/app/shared/state/rule-events.state.ts rename to frontend/app/shared/state/rule-events.state.ts diff --git a/src/Squidex/app/shared/state/rules.state.spec.ts b/frontend/app/shared/state/rules.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/rules.state.spec.ts rename to frontend/app/shared/state/rules.state.spec.ts diff --git a/src/Squidex/app/shared/state/rules.state.ts b/frontend/app/shared/state/rules.state.ts similarity index 100% rename from src/Squidex/app/shared/state/rules.state.ts rename to frontend/app/shared/state/rules.state.ts diff --git a/src/Squidex/app/shared/state/schema-tag-converter.ts b/frontend/app/shared/state/schema-tag-converter.ts similarity index 100% rename from src/Squidex/app/shared/state/schema-tag-converter.ts rename to frontend/app/shared/state/schema-tag-converter.ts diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/schemas.forms.ts rename to frontend/app/shared/state/schemas.forms.ts diff --git a/src/Squidex/app/shared/state/schemas.state.spec.ts b/frontend/app/shared/state/schemas.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/schemas.state.spec.ts rename to frontend/app/shared/state/schemas.state.spec.ts diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/frontend/app/shared/state/schemas.state.ts similarity index 100% rename from src/Squidex/app/shared/state/schemas.state.ts rename to frontend/app/shared/state/schemas.state.ts diff --git a/src/Squidex/app/shared/state/ui.state.spec.ts b/frontend/app/shared/state/ui.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/ui.state.spec.ts rename to frontend/app/shared/state/ui.state.spec.ts diff --git a/src/Squidex/app/shared/state/ui.state.ts b/frontend/app/shared/state/ui.state.ts similarity index 100% rename from src/Squidex/app/shared/state/ui.state.ts rename to frontend/app/shared/state/ui.state.ts diff --git a/src/Squidex/app/shared/state/workflows.forms.ts b/frontend/app/shared/state/workflows.forms.ts similarity index 100% rename from src/Squidex/app/shared/state/workflows.forms.ts rename to frontend/app/shared/state/workflows.forms.ts diff --git a/src/Squidex/app/shared/state/workflows.state.spec.ts b/frontend/app/shared/state/workflows.state.spec.ts similarity index 100% rename from src/Squidex/app/shared/state/workflows.state.spec.ts rename to frontend/app/shared/state/workflows.state.spec.ts diff --git a/src/Squidex/app/shared/state/workflows.state.ts b/frontend/app/shared/state/workflows.state.ts similarity index 100% rename from src/Squidex/app/shared/state/workflows.state.ts rename to frontend/app/shared/state/workflows.state.ts diff --git a/src/Squidex/app/shared/utils/messages.ts b/frontend/app/shared/utils/messages.ts similarity index 100% rename from src/Squidex/app/shared/utils/messages.ts rename to frontend/app/shared/utils/messages.ts diff --git a/src/Squidex/app/shell/declarations.ts b/frontend/app/shell/declarations.ts similarity index 100% rename from src/Squidex/app/shell/declarations.ts rename to frontend/app/shell/declarations.ts diff --git a/src/Squidex/app/shell/index.ts b/frontend/app/shell/index.ts similarity index 100% rename from src/Squidex/app/shell/index.ts rename to frontend/app/shell/index.ts diff --git a/src/Squidex/app/shell/module.ts b/frontend/app/shell/module.ts similarity index 100% rename from src/Squidex/app/shell/module.ts rename to frontend/app/shell/module.ts diff --git a/src/Squidex/app/shell/pages/app/app-area.component.html b/frontend/app/shell/pages/app/app-area.component.html similarity index 100% rename from src/Squidex/app/shell/pages/app/app-area.component.html rename to frontend/app/shell/pages/app/app-area.component.html diff --git a/src/Squidex/app/shell/pages/app/app-area.component.scss b/frontend/app/shell/pages/app/app-area.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/app/app-area.component.scss rename to frontend/app/shell/pages/app/app-area.component.scss diff --git a/src/Squidex/app/shell/pages/app/app-area.component.ts b/frontend/app/shell/pages/app/app-area.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/app/app-area.component.ts rename to frontend/app/shell/pages/app/app-area.component.ts diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/frontend/app/shell/pages/app/left-menu.component.html similarity index 100% rename from src/Squidex/app/shell/pages/app/left-menu.component.html rename to frontend/app/shell/pages/app/left-menu.component.html diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.scss b/frontend/app/shell/pages/app/left-menu.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/app/left-menu.component.scss rename to frontend/app/shell/pages/app/left-menu.component.scss diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.ts b/frontend/app/shell/pages/app/left-menu.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/app/left-menu.component.ts rename to frontend/app/shell/pages/app/left-menu.component.ts diff --git a/src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts b/frontend/app/shell/pages/forbidden/forbidden-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/forbidden/forbidden-page.component.ts rename to frontend/app/shell/pages/forbidden/forbidden-page.component.ts diff --git a/src/Squidex/app/shell/pages/home/home-page.component.html b/frontend/app/shell/pages/home/home-page.component.html similarity index 100% rename from src/Squidex/app/shell/pages/home/home-page.component.html rename to frontend/app/shell/pages/home/home-page.component.html diff --git a/src/Squidex/app/shell/pages/home/home-page.component.scss b/frontend/app/shell/pages/home/home-page.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/home/home-page.component.scss rename to frontend/app/shell/pages/home/home-page.component.scss diff --git a/src/Squidex/app/shell/pages/home/home-page.component.ts b/frontend/app/shell/pages/home/home-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/home/home-page.component.ts rename to frontend/app/shell/pages/home/home-page.component.ts diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.html b/frontend/app/shell/pages/internal/apps-menu.component.html similarity index 100% rename from src/Squidex/app/shell/pages/internal/apps-menu.component.html rename to frontend/app/shell/pages/internal/apps-menu.component.html diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.scss b/frontend/app/shell/pages/internal/apps-menu.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/internal/apps-menu.component.scss rename to frontend/app/shell/pages/internal/apps-menu.component.scss diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts b/frontend/app/shell/pages/internal/apps-menu.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/internal/apps-menu.component.ts rename to frontend/app/shell/pages/internal/apps-menu.component.ts diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.html b/frontend/app/shell/pages/internal/internal-area.component.html similarity index 100% rename from src/Squidex/app/shell/pages/internal/internal-area.component.html rename to frontend/app/shell/pages/internal/internal-area.component.html diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.scss b/frontend/app/shell/pages/internal/internal-area.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/internal/internal-area.component.scss rename to frontend/app/shell/pages/internal/internal-area.component.scss diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.ts b/frontend/app/shell/pages/internal/internal-area.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/internal/internal-area.component.ts rename to frontend/app/shell/pages/internal/internal-area.component.ts diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.html b/frontend/app/shell/pages/internal/profile-menu.component.html similarity index 100% rename from src/Squidex/app/shell/pages/internal/profile-menu.component.html rename to frontend/app/shell/pages/internal/profile-menu.component.html diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.scss b/frontend/app/shell/pages/internal/profile-menu.component.scss similarity index 100% rename from src/Squidex/app/shell/pages/internal/profile-menu.component.scss rename to frontend/app/shell/pages/internal/profile-menu.component.scss diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts b/frontend/app/shell/pages/internal/profile-menu.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/internal/profile-menu.component.ts rename to frontend/app/shell/pages/internal/profile-menu.component.ts diff --git a/src/Squidex/app/shell/pages/login/login-page.component.ts b/frontend/app/shell/pages/login/login-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/login/login-page.component.ts rename to frontend/app/shell/pages/login/login-page.component.ts diff --git a/src/Squidex/app/shell/pages/logout/logout-page.component.ts b/frontend/app/shell/pages/logout/logout-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/logout/logout-page.component.ts rename to frontend/app/shell/pages/logout/logout-page.component.ts diff --git a/src/Squidex/app/shell/pages/not-found/not-found-page.component.ts b/frontend/app/shell/pages/not-found/not-found-page.component.ts similarity index 100% rename from src/Squidex/app/shell/pages/not-found/not-found-page.component.ts rename to frontend/app/shell/pages/not-found/not-found-page.component.ts diff --git a/src/Squidex/app/shims.ts b/frontend/app/shims.ts similarity index 100% rename from src/Squidex/app/shims.ts rename to frontend/app/shims.ts diff --git a/src/Squidex/app/theme/_bootstrap-vars.scss b/frontend/app/theme/_bootstrap-vars.scss similarity index 100% rename from src/Squidex/app/theme/_bootstrap-vars.scss rename to frontend/app/theme/_bootstrap-vars.scss diff --git a/src/Squidex/app/theme/_bootstrap.scss b/frontend/app/theme/_bootstrap.scss similarity index 100% rename from src/Squidex/app/theme/_bootstrap.scss rename to frontend/app/theme/_bootstrap.scss diff --git a/src/Squidex/app/theme/_common.scss b/frontend/app/theme/_common.scss similarity index 100% rename from src/Squidex/app/theme/_common.scss rename to frontend/app/theme/_common.scss diff --git a/src/Squidex/app/theme/_forms.scss b/frontend/app/theme/_forms.scss similarity index 100% rename from src/Squidex/app/theme/_forms.scss rename to frontend/app/theme/_forms.scss diff --git a/src/Squidex/app/theme/_lists.scss b/frontend/app/theme/_lists.scss similarity index 100% rename from src/Squidex/app/theme/_lists.scss rename to frontend/app/theme/_lists.scss diff --git a/src/Squidex/app/theme/_mixins.scss b/frontend/app/theme/_mixins.scss similarity index 100% rename from src/Squidex/app/theme/_mixins.scss rename to frontend/app/theme/_mixins.scss diff --git a/src/Squidex/app/theme/_panels.scss b/frontend/app/theme/_panels.scss similarity index 100% rename from src/Squidex/app/theme/_panels.scss rename to frontend/app/theme/_panels.scss diff --git a/src/Squidex/app/theme/_static.scss b/frontend/app/theme/_static.scss similarity index 100% rename from src/Squidex/app/theme/_static.scss rename to frontend/app/theme/_static.scss diff --git a/src/Squidex/app/theme/_vars.scss b/frontend/app/theme/_vars.scss similarity index 100% rename from src/Squidex/app/theme/_vars.scss rename to frontend/app/theme/_vars.scss diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.css b/frontend/app/theme/icomoon/demo-files/demo.css similarity index 100% rename from src/Squidex/app/theme/icomoon/demo-files/demo.css rename to frontend/app/theme/icomoon/demo-files/demo.css diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.js b/frontend/app/theme/icomoon/demo-files/demo.js similarity index 100% rename from src/Squidex/app/theme/icomoon/demo-files/demo.js rename to frontend/app/theme/icomoon/demo-files/demo.js diff --git a/src/Squidex/app/theme/icomoon/demo.html b/frontend/app/theme/icomoon/demo.html similarity index 100% rename from src/Squidex/app/theme/icomoon/demo.html rename to frontend/app/theme/icomoon/demo.html diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/frontend/app/theme/icomoon/fonts/icomoon.eot similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.eot rename to frontend/app/theme/icomoon/fonts/icomoon.eot diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/frontend/app/theme/icomoon/fonts/icomoon.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.svg rename to frontend/app/theme/icomoon/fonts/icomoon.svg diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/frontend/app/theme/icomoon/fonts/icomoon.ttf similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.ttf rename to frontend/app/theme/icomoon/fonts/icomoon.ttf diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff b/frontend/app/theme/icomoon/fonts/icomoon.woff similarity index 100% rename from src/Squidex/app/theme/icomoon/fonts/icomoon.woff rename to frontend/app/theme/icomoon/fonts/icomoon.woff diff --git a/src/Squidex/app/theme/icomoon/icons/action-Algolia.svg b/frontend/app/theme/icomoon/icons/action-Algolia.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/action-Algolia.svg rename to frontend/app/theme/icomoon/icons/action-Algolia.svg diff --git a/src/Squidex/app/theme/icomoon/icons/action-Fastly.svg b/frontend/app/theme/icomoon/icons/action-Fastly.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/action-Fastly.svg rename to frontend/app/theme/icomoon/icons/action-Fastly.svg diff --git a/src/Squidex/app/theme/icomoon/icons/activity.svg b/frontend/app/theme/icomoon/icons/activity.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/activity.svg rename to frontend/app/theme/icomoon/icons/activity.svg diff --git a/src/Squidex/app/theme/icomoon/icons/add-app.svg b/frontend/app/theme/icomoon/icons/add-app.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/add-app.svg rename to frontend/app/theme/icomoon/icons/add-app.svg diff --git a/src/Squidex/app/theme/icomoon/icons/add.svg b/frontend/app/theme/icomoon/icons/add.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/add.svg rename to frontend/app/theme/icomoon/icons/add.svg diff --git a/src/Squidex/app/theme/icomoon/icons/api.svg b/frontend/app/theme/icomoon/icons/api.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/api.svg rename to frontend/app/theme/icomoon/icons/api.svg diff --git a/src/Squidex/app/theme/icomoon/icons/assets.svg b/frontend/app/theme/icomoon/icons/assets.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/assets.svg rename to frontend/app/theme/icomoon/icons/assets.svg diff --git a/src/Squidex/app/theme/icomoon/icons/caret-bottom.svg b/frontend/app/theme/icomoon/icons/caret-bottom.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/caret-bottom.svg rename to frontend/app/theme/icomoon/icons/caret-bottom.svg diff --git a/src/Squidex/app/theme/icomoon/icons/caret-top.svg b/frontend/app/theme/icomoon/icons/caret-top.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/caret-top.svg rename to frontend/app/theme/icomoon/icons/caret-top.svg diff --git a/src/Squidex/app/theme/icomoon/icons/check-circle-filled.svg b/frontend/app/theme/icomoon/icons/check-circle-filled.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/check-circle-filled.svg rename to frontend/app/theme/icomoon/icons/check-circle-filled.svg diff --git a/src/Squidex/app/theme/icomoon/icons/check-circle.svg b/frontend/app/theme/icomoon/icons/check-circle.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/check-circle.svg rename to frontend/app/theme/icomoon/icons/check-circle.svg diff --git a/src/Squidex/app/theme/icomoon/icons/client.svg b/frontend/app/theme/icomoon/icons/client.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/client.svg rename to frontend/app/theme/icomoon/icons/client.svg diff --git a/src/Squidex/app/theme/icomoon/icons/close.svg b/frontend/app/theme/icomoon/icons/close.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/close.svg rename to frontend/app/theme/icomoon/icons/close.svg diff --git a/src/Squidex/app/theme/icomoon/icons/contents.svg b/frontend/app/theme/icomoon/icons/contents.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/contents.svg rename to frontend/app/theme/icomoon/icons/contents.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Checkbox.svg b/frontend/app/theme/icomoon/icons/control-Checkbox.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Checkbox.svg rename to frontend/app/theme/icomoon/icons/control-Checkbox.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Checkboxes.svg b/frontend/app/theme/icomoon/icons/control-Checkboxes.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Checkboxes.svg rename to frontend/app/theme/icomoon/icons/control-Checkboxes.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Date.svg b/frontend/app/theme/icomoon/icons/control-Date.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Date.svg rename to frontend/app/theme/icomoon/icons/control-Date.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-DateTime.svg b/frontend/app/theme/icomoon/icons/control-DateTime.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-DateTime.svg rename to frontend/app/theme/icomoon/icons/control-DateTime.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Dropdown.svg b/frontend/app/theme/icomoon/icons/control-Dropdown.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Dropdown.svg rename to frontend/app/theme/icomoon/icons/control-Dropdown.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Html.svg b/frontend/app/theme/icomoon/icons/control-Html.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Html.svg rename to frontend/app/theme/icomoon/icons/control-Html.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Input.svg b/frontend/app/theme/icomoon/icons/control-Input.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Input.svg rename to frontend/app/theme/icomoon/icons/control-Input.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Markdown.svg b/frontend/app/theme/icomoon/icons/control-Markdown.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Markdown.svg rename to frontend/app/theme/icomoon/icons/control-Markdown.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Radio.svg b/frontend/app/theme/icomoon/icons/control-Radio.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Radio.svg rename to frontend/app/theme/icomoon/icons/control-Radio.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-RichText.svg b/frontend/app/theme/icomoon/icons/control-RichText.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-RichText.svg rename to frontend/app/theme/icomoon/icons/control-RichText.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Slug.svg b/frontend/app/theme/icomoon/icons/control-Slug.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Slug.svg rename to frontend/app/theme/icomoon/icons/control-Slug.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Tags.svg b/frontend/app/theme/icomoon/icons/control-Tags.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Tags.svg rename to frontend/app/theme/icomoon/icons/control-Tags.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-TextArea.svg b/frontend/app/theme/icomoon/icons/control-TextArea.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-TextArea.svg rename to frontend/app/theme/icomoon/icons/control-TextArea.svg diff --git a/src/Squidex/app/theme/icomoon/icons/control-Toggle.svg b/frontend/app/theme/icomoon/icons/control-Toggle.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/control-Toggle.svg rename to frontend/app/theme/icomoon/icons/control-Toggle.svg diff --git a/src/Squidex/app/theme/icomoon/icons/copy.svg b/frontend/app/theme/icomoon/icons/copy.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/copy.svg rename to frontend/app/theme/icomoon/icons/copy.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-api.svg b/frontend/app/theme/icomoon/icons/dashboard-api.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-api.svg rename to frontend/app/theme/icomoon/icons/dashboard-api.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-feedback.svg b/frontend/app/theme/icomoon/icons/dashboard-feedback.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-feedback.svg rename to frontend/app/theme/icomoon/icons/dashboard-feedback.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-github.svg b/frontend/app/theme/icomoon/icons/dashboard-github.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-github.svg rename to frontend/app/theme/icomoon/icons/dashboard-github.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard-schema.svg b/frontend/app/theme/icomoon/icons/dashboard-schema.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard-schema.svg rename to frontend/app/theme/icomoon/icons/dashboard-schema.svg diff --git a/src/Squidex/app/theme/icomoon/icons/dashboard.svg b/frontend/app/theme/icomoon/icons/dashboard.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/dashboard.svg rename to frontend/app/theme/icomoon/icons/dashboard.svg diff --git a/src/Squidex/app/theme/icomoon/icons/delete-filled.svg b/frontend/app/theme/icomoon/icons/delete-filled.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/delete-filled.svg rename to frontend/app/theme/icomoon/icons/delete-filled.svg diff --git a/src/Squidex/app/theme/icomoon/icons/delete.svg b/frontend/app/theme/icomoon/icons/delete.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/delete.svg rename to frontend/app/theme/icomoon/icons/delete.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-delete.svg b/frontend/app/theme/icomoon/icons/document-delete.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-delete.svg rename to frontend/app/theme/icomoon/icons/document-delete.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-disable.svg b/frontend/app/theme/icomoon/icons/document-disable.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-disable.svg rename to frontend/app/theme/icomoon/icons/document-disable.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-lock.svg b/frontend/app/theme/icomoon/icons/document-lock.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-lock.svg rename to frontend/app/theme/icomoon/icons/document-lock.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-publish.svg b/frontend/app/theme/icomoon/icons/document-publish.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-publish.svg rename to frontend/app/theme/icomoon/icons/document-publish.svg diff --git a/src/Squidex/app/theme/icomoon/icons/document-unpublish.svg b/frontend/app/theme/icomoon/icons/document-unpublish.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/document-unpublish.svg rename to frontend/app/theme/icomoon/icons/document-unpublish.svg diff --git a/src/Squidex/app/theme/icomoon/icons/drag.svg b/frontend/app/theme/icomoon/icons/drag.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/drag.svg rename to frontend/app/theme/icomoon/icons/drag.svg diff --git a/src/Squidex/app/theme/icomoon/icons/fastly.svg b/frontend/app/theme/icomoon/icons/fastly.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/fastly.svg rename to frontend/app/theme/icomoon/icons/fastly.svg diff --git a/src/Squidex/app/theme/icomoon/icons/filter.svg b/frontend/app/theme/icomoon/icons/filter.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/filter.svg rename to frontend/app/theme/icomoon/icons/filter.svg diff --git a/src/Squidex/app/theme/icomoon/icons/help.svg b/frontend/app/theme/icomoon/icons/help.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/help.svg rename to frontend/app/theme/icomoon/icons/help.svg diff --git a/src/Squidex/app/theme/icomoon/icons/hide-all.svg b/frontend/app/theme/icomoon/icons/hide-all.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/hide-all.svg rename to frontend/app/theme/icomoon/icons/hide-all.svg diff --git a/src/Squidex/app/theme/icomoon/icons/hide.svg b/frontend/app/theme/icomoon/icons/hide.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/hide.svg rename to frontend/app/theme/icomoon/icons/hide.svg diff --git a/src/Squidex/app/theme/icomoon/icons/json.svg b/frontend/app/theme/icomoon/icons/json.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/json.svg rename to frontend/app/theme/icomoon/icons/json.svg diff --git a/src/Squidex/app/theme/icomoon/icons/location.svg b/frontend/app/theme/icomoon/icons/location.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/location.svg rename to frontend/app/theme/icomoon/icons/location.svg diff --git a/src/Squidex/app/theme/icomoon/icons/logo.svg b/frontend/app/theme/icomoon/icons/logo.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/logo.svg rename to frontend/app/theme/icomoon/icons/logo.svg diff --git a/src/Squidex/app/theme/icomoon/icons/media.svg b/frontend/app/theme/icomoon/icons/media.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/media.svg rename to frontend/app/theme/icomoon/icons/media.svg diff --git a/src/Squidex/app/theme/icomoon/icons/more.svg b/frontend/app/theme/icomoon/icons/more.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/more.svg rename to frontend/app/theme/icomoon/icons/more.svg diff --git a/src/Squidex/app/theme/icomoon/icons/multiple-content.svg b/frontend/app/theme/icomoon/icons/multiple-content.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/multiple-content.svg rename to frontend/app/theme/icomoon/icons/multiple-content.svg diff --git a/src/Squidex/app/theme/icomoon/icons/orleans.svg b/frontend/app/theme/icomoon/icons/orleans.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/orleans.svg rename to frontend/app/theme/icomoon/icons/orleans.svg diff --git a/src/Squidex/app/theme/icomoon/icons/pencil.svg b/frontend/app/theme/icomoon/icons/pencil.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/pencil.svg rename to frontend/app/theme/icomoon/icons/pencil.svg diff --git a/src/Squidex/app/theme/icomoon/icons/reference.svg b/frontend/app/theme/icomoon/icons/reference.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/reference.svg rename to frontend/app/theme/icomoon/icons/reference.svg diff --git a/src/Squidex/app/theme/icomoon/icons/schemas.svg b/frontend/app/theme/icomoon/icons/schemas.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/schemas.svg rename to frontend/app/theme/icomoon/icons/schemas.svg diff --git a/src/Squidex/app/theme/icomoon/icons/search.svg b/frontend/app/theme/icomoon/icons/search.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/search.svg rename to frontend/app/theme/icomoon/icons/search.svg diff --git a/src/Squidex/app/theme/icomoon/icons/settings.svg b/frontend/app/theme/icomoon/icons/settings.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/settings.svg rename to frontend/app/theme/icomoon/icons/settings.svg diff --git a/src/Squidex/app/theme/icomoon/icons/show-all.svg b/frontend/app/theme/icomoon/icons/show-all.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/show-all.svg rename to frontend/app/theme/icomoon/icons/show-all.svg diff --git a/src/Squidex/app/theme/icomoon/icons/show.svg b/frontend/app/theme/icomoon/icons/show.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/show.svg rename to frontend/app/theme/icomoon/icons/show.svg diff --git a/src/Squidex/app/theme/icomoon/icons/single-content.svg b/frontend/app/theme/icomoon/icons/single-content.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/single-content.svg rename to frontend/app/theme/icomoon/icons/single-content.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Array.svg b/frontend/app/theme/icomoon/icons/type-Array.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Array.svg rename to frontend/app/theme/icomoon/icons/type-Array.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Boolean.svg b/frontend/app/theme/icomoon/icons/type-Boolean.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Boolean.svg rename to frontend/app/theme/icomoon/icons/type-Boolean.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-DateTime.svg b/frontend/app/theme/icomoon/icons/type-DateTime.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-DateTime.svg rename to frontend/app/theme/icomoon/icons/type-DateTime.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Number.svg b/frontend/app/theme/icomoon/icons/type-Number.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Number.svg rename to frontend/app/theme/icomoon/icons/type-Number.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-String.svg b/frontend/app/theme/icomoon/icons/type-String.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-String.svg rename to frontend/app/theme/icomoon/icons/type-String.svg diff --git a/src/Squidex/app/theme/icomoon/icons/type-Tags.svg b/frontend/app/theme/icomoon/icons/type-Tags.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/type-Tags.svg rename to frontend/app/theme/icomoon/icons/type-Tags.svg diff --git a/src/Squidex/app/theme/icomoon/icons/user-o.svg b/frontend/app/theme/icomoon/icons/user-o.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/user-o.svg rename to frontend/app/theme/icomoon/icons/user-o.svg diff --git a/src/Squidex/app/theme/icomoon/icons/user.svg b/frontend/app/theme/icomoon/icons/user.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/user.svg rename to frontend/app/theme/icomoon/icons/user.svg diff --git a/src/Squidex/app/theme/icomoon/icons/webhooks.svg b/frontend/app/theme/icomoon/icons/webhooks.svg similarity index 100% rename from src/Squidex/app/theme/icomoon/icons/webhooks.svg rename to frontend/app/theme/icomoon/icons/webhooks.svg diff --git a/src/Squidex/app/theme/icomoon/selection.json b/frontend/app/theme/icomoon/selection.json similarity index 100% rename from src/Squidex/app/theme/icomoon/selection.json rename to frontend/app/theme/icomoon/selection.json diff --git a/src/Squidex/app/theme/icomoon/style.css b/frontend/app/theme/icomoon/style.css similarity index 100% rename from src/Squidex/app/theme/icomoon/style.css rename to frontend/app/theme/icomoon/style.css diff --git a/src/Squidex/app/theme/theme.scss b/frontend/app/theme/theme.scss similarity index 100% rename from src/Squidex/app/theme/theme.scss rename to frontend/app/theme/theme.scss diff --git a/src/Squidex/karma.conf.js b/frontend/karma.conf.js similarity index 100% rename from src/Squidex/karma.conf.js rename to frontend/karma.conf.js diff --git a/src/Squidex/karma.coverage.conf.js b/frontend/karma.coverage.conf.js similarity index 100% rename from src/Squidex/karma.coverage.conf.js rename to frontend/karma.coverage.conf.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..b125a6e1b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,17012 @@ +{ + "name": "squidex", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/build-optimizer": { + "version": "0.803.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.803.8.tgz", + "integrity": "sha512-UiMxl1wI3acqIoRkC0WA0qpab+ni6SlCaB4UIwfD1H/FdzU80P04AIUuJS7StxjbwVkVtA05kcfgmqzP8yBMVg==", + "dev": true, + "requires": { + "loader-utils": "1.2.3", + "source-map": "0.7.3", + "tslib": "1.10.0", + "typescript": "3.5.3", + "webpack-sources": "1.4.3" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + } + } + }, + "@angular-devkit/core": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.8.tgz", + "integrity": "sha512-HwlMRr6qANwhOJS+5rGgQ2lmP4nj2C4cbUc0LlA09Cdbq0RnDquUFVqHF6h81FUKFW1D5qDehWYHNOVq8+gTkQ==", + "dev": true, + "requires": { + "ajv": "6.10.2", + "fast-json-stable-stringify": "2.0.0", + "magic-string": "0.25.3", + "rxjs": "6.4.0", + "source-map": "0.7.3" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "magic-string": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", + "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "@angular/animations": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-8.2.9.tgz", + "integrity": "sha512-l30AF0d9P5okTPM1wieUHgcnDyGSNvyaBcxXSOkT790wAP2v5zs7VrKq9Lm+ICu4Nkx07KrOr5XLUHhqsg3VXA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/cdk": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.3.tgz", + "integrity": "sha512-ZwO5Sn720RA2YvBqud0JAHkZXjmjxM0yNzCO8RVtRE9i8Gl26Wk0j0nQeJkVm4zwv2QO8MwbKUKGTMt8evsokA==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^1.7.1" + } + }, + "@angular/common": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.2.9.tgz", + "integrity": "sha512-76WDU1USlI5vAzqCJ3gxCQGuu57aJEggNk/xoWmQEXipiFTFBh2wSKn/dE6Txr/q3COTPIcrmb9OCeal5kQPIA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.9.tgz", + "integrity": "sha512-oQho19DnOhEDNerCOGuGK95tcZ2oy4dSA5SykJmmniRnZzPM2++bJD32qJehXHy1K+3hv2zN9x7HPhqT3ljT6g==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/compiler-cli": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.9.tgz", + "integrity": "sha512-tqGBKPf3SRYNEGGJbmjom//U/eAjnecDhGUw6o+VkYE/wxYd9pPcLmcEwwyXBpIPJAsN8RsjTikPuH0gcNE8bw==", + "dev": true, + "requires": { + "canonical-path": "1.0.0", + "chokidar": "^2.1.1", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.7.2", + "magic-string": "^0.25.0", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "source-map": "^0.6.1", + "tslib": "^1.9.0", + "yargs": "13.1.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } + } + }, + "@angular/core": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.2.9.tgz", + "integrity": "sha512-GpHAuLOlN9iioELCQBmAsjETTUCyFgVUI3LXwh3e63jnpd+ZuuZcZbjfTYhtgYVNMetn7cVEO6p88eb7qvpUWQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/forms": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.2.9.tgz", + "integrity": "sha512-kAdBuApC9PPOdPI8BmNhxCraAkXGbX/PkVan8pQ5xdumvgGqvVjbJvLaUSbJROPtgCRlQyiEDrHFd4gk/WU76A==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/http": { + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.15.tgz", + "integrity": "sha512-TR7PEdmLWNIre3Zn8lvyb4lSrvPUJhKLystLnp4hBMcWsJqq5iK8S3bnlR4viZ9HMlf7bW7+Hm4SI6aB3tdUtw==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.9.tgz", + "integrity": "sha512-k3aNZy0OTqGn7HlHHV52QF6ZAP/VlQhWGD2u5e1dWIWMq39kdkdSCNu5tiuAf5hIzMBiSQ0tjnuVWA4MuDBYIQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.9.tgz", + "integrity": "sha512-GbE4TUy4n/a8yp8fLWwdG/QnjUPZZ8VufItZ7GvOpoyknzegvka111dLctvMoPzSAsrKyShL6cryuyDC5PShUA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@angular/platform-server": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-8.2.9.tgz", + "integrity": "sha512-rr6h82+DdUGhpsF3WT3eLk5itjZDXe7SiNtRGHkPj+yTyFAxuTKA3cX0N7LWsGGIFax+s1vQhMreV4YcyHKGPQ==", + "requires": { + "domino": "^2.1.2", + "tslib": "^1.9.0", + "xhr2": "^0.1.4" + } + }, + "@angular/router": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-8.2.9.tgz", + "integrity": "sha512-4P60CWNB/jxGjDBEuYN0Jobt76QlebAQeFBTDswRVwRlq/WJT4QhL3a8AVIRsHn9bQII0LUt/ZQBBPxn7h9lSA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", + "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", + "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "dev": true + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", + "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.5", + "@babel/types": "^7.5.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", + "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, + "@ngtools/webpack": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.8.tgz", + "integrity": "sha512-jLN4/Abue+Ro/K2SF0TpHOXnFHGuaHQ4aL6QG++moZXavBxRdc2E+PDjtuaMaS1llLHs5C5GX+Ve9ueEFhWoeQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "8.3.8", + "enhanced-resolve": "4.1.0", + "rxjs": "6.4.0", + "tree-kill": "1.2.1", + "webpack-sources": "1.4.3" + }, + "dependencies": { + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "@types/core-js": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.2.tgz", + "integrity": "sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/jasmine": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.2.tgz", + "integrity": "sha512-SaSSGOzwUnBEn64c+HTyVTJhRf8F1CXZLnxYx2ww3UrgGBmEEw38RSux2l3fYiT9brVLP67DU5omWA6V9OHI5Q==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/marked": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.6.5.tgz", + "integrity": "sha512-6kBKf64aVfx93UJrcyEZ+OBM5nGv4RLsI6sR1Ar34bpgvGVRoyTgpxn4ZmtxOM5aDTAaaznYuYUH8bUX3Nk3YA==", + "dev": true + }, + "@types/mersenne-twister": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/mersenne-twister/-/mersenne-twister-1.1.2.tgz", + "integrity": "sha512-7KMIfSkMpaVExbzJRLUXHMO4hkFWbbspHPREk8I6pBxiNN+3+l6eAEClMCIPIo2KjCkR0rjYfXppr6+wKdTwpA==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mousetrap": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.0.tgz", + "integrity": "sha512-Jn2cF8X6RAMiSmJaATGjf2r3GzIfpZQpvnQhKprQ5sAbMaNXc7hc9sA2XHdMl3bEMEQhTV79JVW7n4Pgg7sjtg==", + "dev": true + }, + "@types/node": { + "version": "12.7.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz", + "integrity": "sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "@types/react": { + "version": "16.9.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", + "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz", + "integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/sortablejs": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.7.2.tgz", + "integrity": "sha512-yIxpbtlfhaFi2QyuUK54XcmzDWZf5i11CgTrMO4Vh+sKKZthonizkTcqhADeHdngDNTDVUCYfIcfIvpZRAZY+A==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + } + } + }, + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", + "dev": true, + "requires": { + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0", + "uri-js": "^3.0.2" + } + }, + "ajv-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", + "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", + "dev": true + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "angular2-chartjs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/angular2-chartjs/-/angular2-chartjs-0.5.1.tgz", + "integrity": "sha512-bxEVxVEv7llMcgwuc9jlc5KmuOEngT7ZlUyCddmsXwQQAahrTeNgFJ1Nc1SVQnq2fl2d8efh6m70DqF5beiA+A==", + "requires": { + "chart.js": "^2.3.0" + } + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + } + } + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "app-root-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", + "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==", + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", + "dev": true + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "axobject-query": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", + "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", + "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", + "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000989", + "electron-to-chromium": "^1.3.247", + "node-releases": "^1.1.29" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", + "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", + "dev": true, + "requires": { + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "p-map": "^3.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^2.7.1", + "ssri": "^7.0.0", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30000998", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000998.tgz", + "integrity": "sha512-8Tj5sPZR9kMHeDD9SZXIVr5m9ofufLLCG2Y4QwQrH18GIwG+kCc+zYdlR036ZRkuKjVVetyxeAgGA1xF7XdmzQ==", + "dev": true + }, + "canonical-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", + "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chart.js": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.2.tgz", + "integrity": "sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz", + "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=", + "requires": { + "chartjs-color-string": "^0.5.0", + "color-convert": "^0.5.3" + } + }, + "chartjs-color-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz", + "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==", + "requires": { + "color-name": "^1.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-dependency-plugin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz", + "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.x" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codelyzer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-5.1.2.tgz", + "integrity": "sha512-1z7mtpwxcz5uUqq0HLO0ifj/tz2dWEmeaK+8c5TEZXAwwVxrjjg0118ODCOCCOcpfYaaEHxStNCaWVYo9FUPXw==", + "dev": true, + "requires": { + "app-root-path": "^2.2.1", + "aria-query": "^3.0.0", + "axobject-query": "^2.0.2", + "css-selector-tokenizer": "^0.7.1", + "cssauron": "^1.4.0", + "damerau-levenshtein": "^1.0.4", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.7", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "codemirror": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.0.tgz", + "integrity": "sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==" + }, + "codemirror-graphql": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.8.3.tgz", + "integrity": "sha512-ZipSnPXFKDMThfvfTKTAt1dQmuGctVNann8hTZg6017+vwOcGpIqCuQIZLRDw/Y3zZfCyydRARHgbSydSCXpow==", + "requires": { + "graphql-language-service-interface": "^1.3.2", + "graphql-language-service-parser": "^1.2.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + } + } + }, + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-versions": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz", + "integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "dev": true, + "requires": { + "mime-db": ">= 1.40.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-to-clipboard": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", + "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, + "core-js": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", + "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "cpx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", + "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=", + "requires": { + "babel-runtime": "^6.9.2", + "chokidar": "^1.6.0", + "duplexer": "^0.1.1", + "glob": "^7.0.5", + "glob2base": "^0.0.12", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "resolve": "^1.1.7", + "safe-buffer": "^5.0.1", + "shell-quote": "^1.6.1", + "subarg": "^1.0.0" + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-fetch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz", + "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=", + "requires": { + "node-fetch": "2.1.2", + "whatwg-fetch": "2.0.4" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-loader": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.2.0.tgz", + "integrity": "sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.17", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.0", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "dev": true, + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + } + }, + "css-tree": { + "version": "1.0.0-alpha.33", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz", + "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.5.3" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "X.X.X" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "csstype": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "^0.10.9" + } + }, + "damerau-levenshtein": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", + "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", + "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", + "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", + "dev": true + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz", + "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "dev": true, + "requires": { + "utila": "~0.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domino": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.3.tgz", + "integrity": "sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg==" + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.273", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.273.tgz", + "integrity": "sha512-0kUppiHQvHEENHh+nTtvTt4eXMwcPyWmMaj73GPrSEm3ldKhmmHuOH6IjrmuW6YmyS/fpXcLvMQLNVpqRhpNWw==", + "dev": true + }, + "elliptic": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", + "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", + "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~3.3.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~3.3.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "^1.1.1", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.1" + } + }, + "es5-ext": { + "version": "0.10.46", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", + "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-templates": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", + "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", + "dev": true, + "requires": { + "recast": "~0.11.12", + "through": "~2.3.6" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", + "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "concat-stream": "^1.4.6", + "debug": "^2.1.1", + "doctrine": "^1.2.2", + "es6-map": "^0.1.3", + "escope": "^3.6.0", + "espree": "^3.1.6", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^1.1.1", + "glob": "^7.0.3", + "globals": "^9.2.0", + "ignore": "^3.1.2", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "optionator": "^0.8.1", + "path-is-absolute": "^1.0.0", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.6.0", + "strip-json-comments": "~1.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "shelljs": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", + "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "eventemitter3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", + "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", + "dev": true + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "^2.1.0" + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", + "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.2.0.tgz", + "integrity": "sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", + "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.0", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", + "integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==", + "dev": true, + "requires": { + "debug": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "front-matter": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-2.1.2.tgz", + "integrity": "sha1-91mDufL0E75ljJPf172M5AePXNs=", + "dev": true, + "requires": { + "js-yaml": "^3.4.6" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.0.0.tgz", + "integrity": "sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "requires": { + "find-index": "^0.1.1" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + }, + "dependencies": { + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "gonzales-pe-sl": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz", + "integrity": "sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y=", + "dev": true, + "requires": { + "minimist": "1.1.x" + }, + "dependencies": { + "minimist": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", + "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graphiql": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-0.13.2.tgz", + "integrity": "sha512-4N2HmQQpUfApS1cxrTtoZ15tnR3EW88oUiqmza6GgNQYZZfDdBGphdQlBYsKcjAB/SnIOJort+RA1dB6kf4M7Q==", + "requires": { + "codemirror": "^5.47.0", + "codemirror-graphql": "^0.8.3", + "copy-to-clipboard": "^3.2.0", + "markdown-it": "^8.4.0" + } + }, + "graphql": { + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.4.2.tgz", + "integrity": "sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==", + "requires": { + "iterall": "^1.2.2" + } + }, + "graphql-config": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.0.1.tgz", + "integrity": "sha512-eb4FzlODifHE/Q+91QptAmkGw39wL5ToinJ2556UUsGt2drPc4tzifL+HSnHSaxiIbH8EUhc/Fa6+neinF04qA==", + "requires": { + "graphql-import": "^0.4.4", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, + "graphql-import": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.4.5.tgz", + "integrity": "sha512-G/+I08Qp6/QGTb9qapknCm3yPHV0ZL7wbaalWFpxsfR8ZhZoTBe//LsbsCKlbALQpcMegchpJhpTSKiJjhaVqQ==", + "requires": { + "lodash": "^4.17.4" + } + }, + "graphql-language-service-interface": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-1.3.2.tgz", + "integrity": "sha512-sOxFV5sBSnYtKIFHtlmAHHVdhok7CRbvCPLcuHvL4Q1RSgKRsPpeHUDKU+yCbmlonOKn/RWEKaYWrUY0Sgv70A==", + "requires": { + "graphql-config": "2.0.1", + "graphql-language-service-parser": "^1.2.2", + "graphql-language-service-types": "^1.2.2", + "graphql-language-service-utils": "^1.2.2" + } + }, + "graphql-language-service-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.5.0.tgz", + "integrity": "sha512-DX3B6DfvKa28gJoywtnkkIUdZitWqKqBTrZ6CQV8V5wO3GzJalQKT0J+B56oDkS6MhjLt928Yu8fj63laNWfoA==", + "requires": { + "graphql-config": "2.2.1", + "graphql-language-service-types": "^1.5.0" + }, + "dependencies": { + "graphql-config": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", + "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", + "requires": { + "graphql-import": "^0.7.1", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, + "graphql-import": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", + "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", + "requires": { + "lodash": "^4.17.4", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "graphql-language-service-types": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.5.0.tgz", + "integrity": "sha512-THxB15oPC56zlNVSwv7JCahuSUbI9xnUHdftjOqZOz5588qjlPw/UHWQ8V/k0/XwZvH/TwCkmnBkIRmPVb1S5Q==", + "requires": { + "graphql-config": "2.2.1" + }, + "dependencies": { + "graphql-config": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", + "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", + "requires": { + "graphql-import": "^0.7.1", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, + "graphql-import": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", + "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", + "requires": { + "lodash": "^4.17.4", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "graphql-language-service-utils": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-1.2.2.tgz", + "integrity": "sha512-98hzn1Dg3sSAiB+TuvNwWAoBrzuHs8NylkTK26TFyBjozM5wBZttp+T08OvOt+9hCFYRa43yRPrWcrs78KH9Hw==", + "requires": { + "graphql-config": "2.0.1", + "graphql-language-service-types": "^1.2.2" + } + }, + "graphql-request": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", + "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==", + "requires": { + "cross-fetch": "2.2.2" + } + }, + "handle-thing": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", + "dev": true + }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", + "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", + "dev": true, + "requires": { + "es6-templates": "^0.2.3", + "fastparse": "^1.1.1", + "html-minifier": "^3.5.8", + "loader-utils": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "html-minifier": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.19.tgz", + "integrity": "sha512-Qr2JC9nsjK8oCrEmuB430ZIA8YWbF3D5LSjywD75FTuXmeqacwHgIM8wp3vHYzzPbklSjp53RdmDuzR4ub2HzA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.1.x", + "commander": "2.16.x", + "he": "1.1.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + } + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.1", + "domutils": "1.1", + "readable-stream": "1.0" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "dev": true + }, + "http-proxy": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", + "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "ignore-loader": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", + "integrity": "sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM=", + "dev": true + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", + "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-api": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz", + "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "compare-versions": "^3.4.0", + "fileset": "^2.0.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "minimatch": "^3.0.4", + "once": "^1.4.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-instrumenter-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", + "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", + "dev": true, + "requires": { + "convert-source-map": "^1.5.0", + "istanbul-lib-instrument": "^1.7.3", + "loader-utils": "^1.1.0", + "schema-utils": "^0.3.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "dev": true, + "requires": { + "ajv": "^5.0.0" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "iterall": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", + "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" + }, + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + }, + "jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "karma": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/karma/-/karma-4.3.0.tgz", + "integrity": "sha512-NSPViHOt+RW38oJklvYxQC4BSQsv737oQlr/r06pCM+slDOr4myuI1ivkRmp+3dVpJDfZt2DmaPJ2wkx+ZZuMQ==", + "dev": true, + "requires": { + "bluebird": "^3.3.0", + "body-parser": "^1.16.1", + "braces": "^3.0.2", + "chokidar": "^3.0.0", + "colors": "^1.1.0", + "connect": "^3.6.0", + "core-js": "^3.1.3", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "flatted": "^2.0.0", + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^4.17.14", + "log4js": "^4.0.0", + "mime": "^2.3.1", + "minimatch": "^3.0.2", + "optimist": "^0.6.1", + "qjobs": "^1.1.4", + "range-parser": "^1.2.0", + "rimraf": "^2.6.0", + "safe-buffer": "^5.0.1", + "socket.io": "2.1.1", + "source-map": "^0.6.1", + "tmp": "0.0.33", + "useragent": "2.3.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.1.tgz", + "integrity": "sha512-/j5PPkb5Feyps9e+jo07jUZGvkB5Aj953NrI4s8xSVScrAo/RHeILrtdb4uzR7N6aaFFxxJ+gt8mA8HfNpw76w==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.0", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.1.3" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.0.tgz", + "integrity": "sha512-+iXhW3LuDQsno8dOIrCIT/CBjeBWuP7PXe8w9shnj9Lebny/Gx1ZjVBYwexLz36Ri2jKuXMNpV6CYNh8lHHgrQ==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.3.tgz", + "integrity": "sha512-ZOsfTGkjO2kqeR5Mzr5RYDbTGYneSkdNKX2fOX2P5jF7vMrd/GNnIAUtDldeHHumHUCQ3V05YfWUdxMPAsRu9Q==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "dev": true, + "requires": { + "which": "^1.2.1" + } + }, + "karma-cli": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz", + "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==", + "dev": true, + "requires": { + "resolve": "^1.3.3" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.0.tgz", + "integrity": "sha512-UH0mXPJFJyK5uiK7EkwGtQ8f30lCBAfqRResnZ4pzLJ04SOp4SPlYkmwbbZ6iVJ6sQFVzlDUXlntBEsLRdgZpg==", + "dev": true, + "requires": { + "istanbul-api": "^2.1.6", + "minimatch": "^3.0.4" + } + }, + "karma-htmlfile-reporter": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/karma-htmlfile-reporter/-/karma-htmlfile-reporter-0.3.8.tgz", + "integrity": "sha512-Hd4c/vqPXYjdNYXeDJRMMq2DMMxPxqOR+TPeiLz2qbqO0qCCQMeXwFGhNDFr+GsvYhcOyn7maTbWusUFchS/4A==", + "dev": true, + "requires": { + "xmlbuilder": "^10.0.0" + } + }, + "karma-jasmine": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", + "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", + "dev": true, + "requires": { + "jasmine-core": "^3.3" + } + }, + "karma-jasmine-html-reporter": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.4.2.tgz", + "integrity": "sha512-7g0gPj8+9JepCNJR9WjDyQ2RkZ375jpdurYQyAYv8PorUCadepl8vrD6LmMqOGcM17cnrynBawQYZHaumgDjBw==", + "dev": true + }, + "karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "karma-sourcemap-loader": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz", + "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "karma-webpack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz", + "integrity": "sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.1.0", + "neo-async": "^2.6.1", + "schema-utils": "^1.0.0", + "source-map": "^0.7.3", + "webpack-dev-middleware": "^3.7.0" + }, + "dependencies": { + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "known-css-properties": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.3.0.tgz", + "integrity": "sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ==", + "dev": true + }, + "last-call-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", + "dev": true, + "requires": { + "lodash": "^4.17.5", + "webpack-sources": "^1.1.0" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "log4js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", + "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", + "dev": true, + "requires": { + "date-format": "^2.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.0", + "rfdc": "^1.1.4", + "streamroller": "^1.0.6" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "loglevel": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.4.tgz", + "integrity": "sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "magic-string": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz", + "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "marked": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", + "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mersenne-twister": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", + "integrity": "sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minipass": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.0.1.tgz", + "integrity": "sha512-2y5okJ4uBsjoD2vAbLKL9EUQPPkC0YMIp+2mZOXG3nBba++pdfJWRxx2Ewirc0pwAJYu4XtWg2EkVo1nRXuO/w==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", + "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "mousetrap": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz", + "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==" + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "ngx-color-picker": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-8.2.0.tgz", + "integrity": "sha512-rzR+cByjNG9M/UskU5vNoH7cUc6oM8STTDFKOZmnlX4ALOuM1+61CBjsNTGETWfo9a/h5mbGX02oh5/iNAa7vA==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-fetch": { + "version": "2.1.2", + "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + }, + "node-forge": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", + "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + } + } + }, + "node-releases": { + "version": "1.1.34", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.34.tgz", + "integrity": "sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "resolve": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", + "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "dev": true + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "oidc-client": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.9.1.tgz", + "integrity": "sha512-AP1BwqASKIYrCBMu9dmNy3OTbhfaiBpy+5hZRbG1dmE2HqpQCp2JiJUNnNGTh2P+cnfVOrC79CGIluD1VMgMzQ==", + "requires": { + "base64-js": "^1.3.0", + "core-js": "^2.6.4", + "crypto-js": "^3.1.9-1", + "uuid": "^3.3.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "optimize-css-assets-webpack-plugin": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", + "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==", + "dev": true, + "requires": { + "cssnano": "^4.1.10", + "last-call-webpack-plugin": "^3.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "dependencies": { + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pikaday": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.0.tgz", + "integrity": "sha512-SgGxMYX0NHj9oQnMaSyAipr2gOrbB4Lfs/TJTb6H6hRHs39/5c5VZi73Q8hr53+vWjdn6HzkWcj8Vtl3c9ziaA==" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "portfinder": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz", + "integrity": "sha512-ekRl7zD2qxYndYflwiryJwMioBI7LI7rVXg3EnLK3sjkouT5eOuhS3gS255XxBksa30VG8UPZYZCdgfGOfkSUg==", + "dev": true, + "requires": { + "async": "^1.5.2", + "debug": "^2.2.0", + "mkdirp": "0.5.x" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.5", + "postcss-selector-parser": "^5.0.0-rc.4", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz", + "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "dependencies": { + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + } + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, + "postinstall-build": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", + "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "progressbar.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/progressbar.js/-/progressbar.js-1.0.1.tgz", + "integrity": "sha1-9/v8GVJA/guzL2972y5/9ADqcfk=", + "requires": { + "shifty": "^1.5.2" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "dev": true + }, + "randomatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz", + "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==", + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "raw-loader": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz", + "integrity": "sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^2.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", + "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "react": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", + "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz", + "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.16.2" + } + }, + "react-is": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "dev": true, + "requires": { + "ast-types": "0.9.6", + "esprima": "~3.1.0", + "private": "~0.1.5", + "source-map": "~0.5.0" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "dev": true, + "requires": { + "css-select": "^1.1.0", + "dom-converter": "~0.1", + "htmlparser2": "~3.3.0", + "strip-ansi": "^3.0.0", + "utila": "~0.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + } + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "rxjs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "requires": { + "tslib": "^1.9.0" + } + }, + "rxjs-tslint": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/rxjs-tslint/-/rxjs-tslint-0.1.7.tgz", + "integrity": "sha512-NnOfqutNfdT7VQnQm32JLYh2gDZjc0gdWZFtrxf/czNGkLKJ1nOO6jbKAFI09W0f9lCtv6P2ozxjbQH8TSPPFQ==", + "dev": true, + "requires": { + "chalk": "^2.4.0", + "optimist": "^0.6.1", + "tslint": "^5.9.1", + "tsutils": "^2.25.0", + "typescript": ">=2.8.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, + "sass-lint": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sass-lint/-/sass-lint-1.13.1.tgz", + "integrity": "sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==", + "dev": true, + "requires": { + "commander": "^2.8.1", + "eslint": "^2.7.0", + "front-matter": "2.1.2", + "fs-extra": "^3.0.1", + "glob": "^7.0.0", + "globule": "^1.0.0", + "gonzales-pe-sl": "^4.2.3", + "js-yaml": "^3.5.4", + "known-css-properties": "^0.3.0", + "lodash.capitalize": "^4.1.0", + "lodash.kebabcase": "^4.0.0", + "merge": "^1.2.0", + "path-is-absolute": "^1.0.0", + "util": "^0.10.3" + }, + "dependencies": { + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "sass-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz", + "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.2.3", + "neo-async": "^2.6.1", + "schema-utils": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.2.0.tgz", + "integrity": "sha512-5EwsCNhfFTZvUreQhx/4vVQpJ/lnCAkgoIHLhSpp4ZirE+4hzFvdJi0FMub6hxbFVBJYSpeVVmon+2e7uEGRrA==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", + "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "dev": true, + "requires": { + "node-forge": "0.9.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz", + "integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shifty": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/shifty/-/shifty-1.5.4.tgz", + "integrity": "sha1-1DYvyRTdKA3fblIr5AiyEgMgg0Y=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "slugify": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.5.tgz", + "integrity": "sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "socket.io": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", + "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "dev": true, + "requires": { + "debug": "~3.1.0", + "engine.io": "~3.2.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.1.1", + "socket.io-parser": "~3.2.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", + "dev": true + }, + "socket.io-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "dev": true, + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.2.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.2.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "socket.io-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.0.1" + } + }, + "sockjs-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", + "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "dev": true, + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", + "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, + "spdy": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", + "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.0.1.tgz", + "integrity": "sha512-FfndBvkXL9AHyGLNzU3r9AvYIBBZ7gm+m+kd0p8cT3/v4OliMAyipZAhLVEv1Zi/k4QFq9CstRGVd9pW/zcHFQ==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "minipass": "^3.0.0" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "streamroller": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", + "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", + "dev": true, + "requires": { + "async": "^2.6.2", + "date-format": "^2.0.0", + "debug": "^3.2.6", + "fs-extra": "^7.0.1", + "lodash": "^4.17.14" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "style-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz", + "integrity": "sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.0.1" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "requires": { + "minimist": "^1.1.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz", + "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.33", + "csso": "^3.5.1", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "dependencies": { + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + } + } + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", + "dev": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "terser": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.4.tgz", + "integrity": "sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", + "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.1.2.tgz", + "integrity": "sha512-MF/C4KABwqYOfRDi87f7gG07GP7Wj/kyiX938UxIGIO6l5mkh8XJL7xtS0hX/CRdVQaZI7ThGUPZbznrCjsGpg==", + "dev": true, + "requires": { + "cacache": "^13.0.0", + "find-cache-dir": "^3.0.0", + "jest-worker": "^24.9.0", + "schema-utils": "^2.4.1", + "serialize-javascript": "^2.1.0", + "source-map": "^0.6.1", + "terser": "^4.3.4", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", + "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "thunky": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", + "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", + "dev": true + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "tree-kill": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", + "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, + "ts-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.0.tgz", + "integrity": "sha512-Da8h3fD+HiZ9GvZJydqzk3mTC9nuOKYlJcpuk+Zv6Y1DPaMvBL+56GRzZFypx2cWrZFMsQr869+Ua2slGoLxvQ==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "tsconfig-paths": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.5.0.tgz", + "integrity": "sha512-JYbN2zK2mxsv+bDVJCvSTxmdrD4R1qkG908SsqqD8TWjPNbSOtko1mnpQFFJo5Rbbc2/oJgDU9Cpkg/ZD7wNYg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "deepmerge": "^2.0.1", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "tsconfig-paths": "^3.4.0" + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tslint": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.0.tgz", + "integrity": "sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "tslint-immutable": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/tslint-immutable/-/tslint-immutable-5.4.0.tgz", + "integrity": "sha512-8lZG7hNYRFOJv/p/Wb8/1cgizWSRpn4W3GSNWUVye9WyeO/LRbxp88pzNO8Een3RCMbHa3o7oW2UWa+Sx6hCBA==", + "dev": true, + "requires": { + "tsutils": "^2.28.0 || ^3.0.0" + } + }, + "tslint-webpack-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslint-webpack-plugin/-/tslint-webpack-plugin-2.1.0.tgz", + "integrity": "sha512-subYgmwihOGftPZS59looqPWdbqMIvsoTy8MeQPeZ7bOdwZfR3AAnVG8/VzpSRly8l/xbPosrX2QKtJEZPt71A==", + "dev": true, + "requires": { + "chalk": "^2.1.0" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + }, + "dependencies": { + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + } + } + }, + "typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "uglify-js": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.7.tgz", + "integrity": "sha512-J0M2i1mQA+ze3EdN9SBi751DNdAXmeFLfJrd/MDIkRc3G3Gbb9OPVSx7GIQvVwfWxQARcYV2DTxIkMyDAk3o9Q==", + "dev": true, + "requires": { + "commander": "~2.16.0", + "source-map": "~0.6.1" + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "requires": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", + "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webpack": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz", + "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.1", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "dev": true + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "serialize-javascript": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", + "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser-webpack-plugin": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", + "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz", + "integrity": "sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A==", + "dev": true, + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz", + "integrity": "sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.2", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + } + } + }, + "webpack-dev-server": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.2.tgz", + "integrity": "sha512-0xxogS7n5jHDQWy0WST0q6Ykp7UGj4YvWh+HVN71JoE7BwPxMZrwgraBvmdEMbDVMBzF0u+mEzn8TQzBm5NYJQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.2.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.4", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.24", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.4.0", + "spdy": "^4.0.1", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "webpack-dev-middleware": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", + "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xhr2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, + "xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.1.0.tgz", + "integrity": "sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "zone.js": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.2.tgz", + "integrity": "sha512-UAYfiuvxLN4oyuqhJwd21Uxb4CNawrq6fPS/05Su5L4G+1TN+HVDJMUHNMobVQDFJRir2cLAODXwluaOKB7HFg==" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..3bf616acf --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,105 @@ +{ + "name": "squidex", + "version": "1.0.0", + "description": "Squidex Headless CMS", + "license": "MIT", + "repository": "https://github.com/SebastianStehle/Squidex", + "scripts": { + "start": "webpack-dev-server --config app-config/webpack.config.js --inline --port 3000 --hot", + "test": "karma start", + "test:coverage": "karma start karma.coverage.conf.js", + "test:clean": "rimraf _test-output", + "tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts", + "build": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js --config app-config/webpack.config.js --env.production", + "build:clean": "rimraf wwwroot/build" + }, + "dependencies": { + "@angular/animations": "8.2.9", + "@angular/cdk": "8.2.3", + "@angular/common": "8.2.9", + "@angular/core": "8.2.9", + "@angular/forms": "8.2.9", + "@angular/http": "7.2.15", + "@angular/platform-browser": "8.2.9", + "@angular/platform-browser-dynamic": "8.2.9", + "@angular/platform-server": "8.2.9", + "@angular/router": "8.2.9", + "angular2-chartjs": "0.5.1", + "babel-polyfill": "6.26.0", + "bootstrap": "4.3.1", + "core-js": "3.2.1", + "graphiql": "0.13.2", + "graphql": "14.4.2", + "marked": "0.7.0", + "mersenne-twister": "1.1.0", + "moment": "2.24.0", + "mousetrap": "1.6.3", + "ngx-color-picker": "8.2.0", + "oidc-client": "1.9.1", + "pikaday": "1.8.0", + "progressbar.js": "1.0.1", + "react": "16.10.2", + "react-dom": "16.10.2", + "rxjs": "6.5.3", + "slugify": "1.3.5", + "tslib": "1.10.0", + "zone.js": "0.10.2" + }, + "devDependencies": { + "@angular-devkit/build-optimizer": "0.803.8", + "@angular/compiler": "8.2.9", + "@angular/compiler-cli": "8.2.9", + "@ngtools/webpack": "8.3.8", + "@types/core-js": "2.5.2", + "@types/jasmine": "3.4.2", + "@types/marked": "0.6.5", + "@types/mersenne-twister": "1.1.2", + "@types/mousetrap": "1.6", + "@types/node": "12.7.11", + "@types/react": "16.9.5", + "@types/react-dom": "16.9.1", + "@types/sortablejs": "1.7.2", + "browserslist": "4.7.0", + "caniuse-lite": "1.0.30000998", + "circular-dependency-plugin": "5.2.0", + "codelyzer": "5.1.2", + "css-loader": "3.2.0", + "file-loader": "4.2.0", + "html-loader": "0.5.5", + "html-webpack-plugin": "3.2.0", + "ignore-loader": "0.1.2", + "istanbul-instrumenter-loader": "3.0.1", + "jasmine-core": "3.5.0", + "karma": "4.3.0", + "karma-chrome-launcher": "3.1.0", + "karma-cli": "2.0.0", + "karma-coverage-istanbul-reporter": "2.1.0", + "karma-htmlfile-reporter": "0.3.8", + "karma-jasmine": "2.0.1", + "karma-jasmine-html-reporter": "1.4.2", + "karma-mocha-reporter": "2.2.5", + "karma-sourcemap-loader": "0.3.7", + "karma-webpack": "4.0.2", + "mini-css-extract-plugin": "0.8.0", + "node-sass": "4.12.0", + "optimize-css-assets-webpack-plugin": "5.0.3", + "raw-loader": "3.1.0", + "rimraf": "3.0.0", + "rxjs-tslint": "0.1.7", + "sass-lint": "1.13.1", + "sass-loader": "8.0.0", + "style-loader": "1.0.0", + "terser-webpack-plugin": "2.1.2", + "ts-loader": "6.2.0", + "tsconfig-paths-webpack-plugin": "3.2.0", + "tslint": "5.20.0", + "tslint-immutable": "5.4.0", + "tslint-webpack-plugin": "2.1.0", + "typemoq": "2.1.0", + "typescript": "3.5.3", + "underscore": "1.9.1", + "webpack": "4.41.0", + "webpack-cli": "3.3.9", + "webpack-dev-server": "3.8.2" + } +} diff --git a/src/Squidex/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from src/Squidex/tsconfig.json rename to frontend/tsconfig.json diff --git a/src/Squidex/tslint.json b/frontend/tslint.json similarity index 100% rename from src/Squidex/tslint.json rename to frontend/tslint.json diff --git a/libs/Dockerfile b/libs/Dockerfile deleted file mode 100644 index 33fb70728..000000000 --- a/libs/Dockerfile +++ /dev/null @@ -1,58 +0,0 @@ -FROM microsoft/dotnet:2.2-sdk - -# Install runtime dependencies -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install official PhantomJS release -RUN set -x \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - && mkdir /srv/var \ - && mkdir /tmp/phantomjs \ - # Download Phantom JS - && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \ - # Copy binaries only - && mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \ - # Create symbol link - # Clean up - && apt-get autoremove -y \ - && apt-get clean all \ - && rm -rf /tmp/* /var/lib/apt/lists/* - -RUN phantomjs --version - -# Install Node -ENV NODE_VERSION 8.9.4 -ENV NODE_DOWNLOAD_SHA 21fb4690e349f82d708ae766def01d7fec1b085ce1f5ab30d9bda8ee126ca8fc -RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" --output nodejs.tar.gz \ - && echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \ - && tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \ - && rm nodejs.tar.gz \ - && ln -s /usr/local/bin/node /usr/local/bin/nodejs - -# Install Google Chrome - -# See https://crbug.com/795759 -RUN apt-get update && apt-get install -yq libgconf-2-4 - -# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others) -RUN apt-get update && apt-get install -y wget --no-install-recommends \ - && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update \ - && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get autoremove -y \ - && rm -rf /src/*.deb - -# It's a good idea to use dumb-init to help prevent zombie chrome processes. -ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init - -RUN chmod +x /usr/local/bin/dumb-init - -# Install puppeteer so it's available in the container. -RUN npm i puppeteer \ No newline at end of file diff --git a/libs/docker-compose.yml b/libs/docker-compose.yml new file mode 100644 index 000000000..bf885957d --- /dev/null +++ b/libs/docker-compose.yml @@ -0,0 +1,29 @@ +version: '2.1' +services: + mongo: + image: mongo:latest + ports: + - "27018:27017" + networks: + - internal + restart: always + + squidex: + image: "squidex" + ports: + - "80:80" + environment: + - URLS__BASEURL=http://localhost + - EVENTSTORE__CONSUME=true + - EVENTSTORE__MONGODB__CONFIGURATION=mongodb://mongo + - STORE__MONGODB__CONFIGURATION=mongodb://mongo + - STORE__TYPE=MongoDB + depends_on: + - mongo + networks: + - internal + restart: unless-stopped + +networks: + internal: + driver: bridge \ No newline at end of file diff --git a/nuget.exe b/nuget.exe deleted file mode 100644 index ec1309c7a..000000000 Binary files a/nuget.exe and /dev/null differ diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs deleted file mode 100644 index 518ecd646..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.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, nameof(secret)); - Guard.NotNullOrEmpty(role, nameof(role)); - - Role = role; - - Secret = secret; - } - - [Pure] - public AppClient Update(string newRole) - { - Guard.NotNullOrEmpty(newRole, nameof(newRole)); - - return new AppClient(Name, Secret, newRole); - } - - [Pure] - public AppClient Rename(string newName) - { - Guard.NotNullOrEmpty(newName, nameof(newName)); - - return new AppClient(newName, Secret, Role); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs deleted file mode 100644 index 3646682a7..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ========================================================================== -// 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 - { - public static readonly AppClients Empty = new AppClients(); - - private AppClients() - { - } - - public AppClients(KeyValuePair[] items) - : base(items) - { - } - - [Pure] - public AppClients Revoke(string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - return new AppClients(Without(id)); - } - - [Pure] - public AppClients Add(string id, AppClient client) - { - Guard.NotNullOrEmpty(id, nameof(id)); - Guard.NotNull(client, nameof(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, nameof(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, nameof(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, nameof(id)); - - if (!TryGetValue(id, out var client)) - { - return this; - } - - return new AppClients(With(id, client.Update(role))); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs deleted file mode 100644 index ee54ae6fb..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// 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 - { - public static readonly AppContributors Empty = new AppContributors(); - - private AppContributors() - { - } - - public AppContributors(KeyValuePair[] items) - : base(items) - { - } - - [Pure] - public AppContributors Assign(string contributorId, string role) - { - Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); - Guard.NotNullOrEmpty(role, nameof(role)); - - return new AppContributors(With(contributorId, role)); - } - - [Pure] - public AppContributors Remove(string contributorId) - { - Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); - - return new AppContributors(Without(contributorId)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs deleted file mode 100644 index b10c7e904..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class AppImage - { - public string MimeType { get; } - - public string Etag { get; } - - public AppImage(string mimeType, string etag = null) - { - Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); - - MimeType = mimeType; - - if (string.IsNullOrWhiteSpace(etag)) - { - Etag = RandomHash.Simple(); - } - else - { - Etag = etag; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs deleted file mode 100644 index 864961903..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.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, nameof(pattern)); - - Pattern = pattern; - - Message = message; - } - - [Pure] - public AppPattern Update(string newName, string newPattern, string newMessage) - { - return new AppPattern(newName, newPattern, newMessage); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs deleted file mode 100644 index cb9e13d3d..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// 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 - { - public static readonly AppPatterns Empty = new AppPatterns(); - - private AppPatterns() - { - } - - public AppPatterns(KeyValuePair[] 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, nameof(name)); - Guard.NotNullOrEmpty(pattern, nameof(pattern)); - - if (!TryGetValue(id, out var appPattern)) - { - return this; - } - - return new AppPatterns(With(id, appPattern.Update(name, pattern, message))); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs deleted file mode 100644 index ab23055bf..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// 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, nameof(owner)); - Guard.NotNullOrEmpty(planId, nameof(planId)); - - Owner = owner; - - PlanId = planId; - } - - public static AppPlan Build(RefToken owner, string planId) - { - if (planId == null) - { - return null; - } - else - { - return new AppPlan(owner, planId); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs deleted file mode 100644 index 4649126c3..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// 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); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs deleted file mode 100644 index c455c07ff..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Newtonsoft.Json; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Apps.Json -{ - public sealed class JsonLanguagesConfig - { - [JsonProperty] - public Dictionary Languages { get; set; } - - [JsonProperty] - public Language Master { get; set; } - - public JsonLanguagesConfig() - { - } - - public JsonLanguagesConfig(LanguagesConfig value) - { - Languages = new Dictionary(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; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs deleted file mode 100644 index ac4bffde9..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// 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 LanguageFallbacks - { - get { return languageFallbacks; } - } - - string IFieldPartitionItem.Key - { - get { return language.Iso2Code; } - } - - string IFieldPartitionItem.Name - { - get { return language.EnglishName; } - } - - IEnumerable IFieldPartitionItem.Fallback - { - get { return LanguageFallbacks.Select(x => x.Iso2Code); } - } - - public LanguageConfig(Language language, bool isOptional = false, IEnumerable fallback = null) - : this(language, isOptional, fallback?.ToArray()) - { - } - - public LanguageConfig(Language language, bool isOptional = false, params Language[] fallback) - { - Guard.NotNull(language, nameof(language)); - - IsOptional = isOptional; - - this.language = language; - this.languageFallbacks = fallback ?? Array.Empty(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs deleted file mode 100644 index cb83980f7..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ========================================================================== -// 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.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 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 IEnumerable.GetEnumerator() - { - return languages.Values.GetEnumerator(); - } - - public int Count - { - get { return languages.Count; } - } - - private LanguagesConfig(ArrayDictionary 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 configs) - { - Guard.NotNull(configs, nameof(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, nameof(language)); - - return new LanguagesConfig(languages, languages[language]); - } - - [Pure] - public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable fallback = null) - { - Guard.NotNull(language, nameof(language)); - - return Set(new LanguageConfig(language, isOptional, fallback)); - } - - [Pure] - public LanguagesConfig Set(LanguageConfig config) - { - Guard.NotNull(config, nameof(config)); - - var newLanguages = - new ArrayDictionary(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, nameof(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, out LanguageConfig config) - { - return languages.TryGetValue(language, out config); - } - - public bool TryGetItem(string key, 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; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs deleted file mode 100644 index 1279367c1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// 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, nameof(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 - { - 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)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs deleted file mode 100644 index bcdb9e226..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ /dev/null @@ -1,179 +0,0 @@ -// ========================================================================== -// 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.Collections; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class Roles - { - private readonly ArrayDictionary inner; - - public static readonly IReadOnlyDictionary Defaults = new Dictionary - { - [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()); - - public int CustomCount - { - get { return inner.Count; } - } - - public Role this[string name] - { - get { return inner[name]; } - } - - public IEnumerable Custom - { - get { return inner.Values; } - } - - public IEnumerable All - { - get { return inner.Values.Union(Defaults.Values); } - } - - private Roles(ArrayDictionary roles) - { - inner = roles; - } - - public Roles(IEnumerable> items) - { - inner = new ArrayDictionary(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, nameof(name)); - Guard.NotNull(permissions, nameof(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, out Role value) - { - Guard.NotNull(app, nameof(app)); - - value = null; - - if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role)) - { - value = role.ForApp(app); - return true; - } - - 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[] Cleaned(IEnumerable> items) - { - return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs deleted file mode 100644 index 61a90f23c..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// 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, nameof(id)); - Guard.NotNull(user, nameof(user)); - Guard.NotNull(text, nameof(text)); - - Id = id; - - Time = time; - Text = text; - - User = user; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs deleted file mode 100644 index fcfe4813e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// 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 : Dictionary, IEquatable> - { - public IEnumerable> ValidValues - { - get { return this.Where(x => x.Value != null); } - } - - protected ContentData(IEqualityComparer comparer) - : base(comparer) - { - } - - protected ContentData(int capacity, IEqualityComparer comparer) - : base(capacity, comparer) - { - } - - protected static TResult MergeTo(TResult target, params TResult[] sources) where TResult : ContentData - { - Guard.NotEmpty(sources, nameof(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) - { - var fieldValue = target.GetOrAddNew(otherValue.Key); - - foreach (var value in otherValue.Value) - { - fieldValue[value.Key] = value.Value; - } - } - } - - return target; - } - - protected static TResult Clean(TResult source, TResult target) where TResult : ContentData - { - 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); - } - - public bool Equals(ContentData other) - { - return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); - } - - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs deleted file mode 100644 index 28dca2ac1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// 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, IEquatable - { - 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, nameof(key)); - - if (Language.IsValidLanguage(key)) - { - this[key] = value; - // this[string.Intern(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(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs deleted file mode 100644 index 0ca33663e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// 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, IEquatable - { - public IdContentData() - : base(EqualityComparer.Default) - { - } - - public IdContentData(int capacity) - : base(capacity, EqualityComparer.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, nameof(id)); - - this[id] = data; - - return this; - } - - public bool Equals(IdContentData other) - { - return base.Equals(other); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs deleted file mode 100644 index 3f170459e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// 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 - { - 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(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."); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs deleted file mode 100644 index 48274e127..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschrnkt) -// 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 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 { Role }; - } - - ReadOnlyCollection roles = null; - - if (rolesList != null && rolesList.Count > 0) - { - roles = new ReadOnlyCollection(rolesList); - } - - return new WorkflowTransition(Expression, roles); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs deleted file mode 100644 index d6afcd95f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class NamedContentData : ContentData, IEquatable - { - 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, nameof(name)); - - this[name] = data; - - return this; - } - - public bool Equals(NamedContentData other) - { - return base.Equals(other); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs deleted file mode 100644 index 32026fc44..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// 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 - { - 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); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs deleted file mode 100644 index a7ba559c7..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// 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(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs deleted file mode 100644 index dae5dfd26..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public sealed class Workflow : Named - { - private const string DefaultName = "Unnamed"; - - public static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); - public static readonly IReadOnlyList EmptySchemaIds = new List(); - public static readonly Workflow Default = CreateDefault(); - public static readonly Workflow Empty = new Workflow(default, EmptySteps); - - public IReadOnlyDictionary Steps { get; } = EmptySteps; - - public IReadOnlyList SchemaIds { get; } = EmptySchemaIds; - - public Status Initial { get; } - - public Workflow( - Status initial, - IReadOnlyDictionary steps, - IReadOnlyList 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.Archived] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Archived, true), - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Published] = new WorkflowTransition() - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [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, 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, out WorkflowStep step) - { - return Steps.TryGetValue(status, out step); - } - - public (Status Key, WorkflowStep) GetInitialStep() - { - return (Initial, Steps[Initial]); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs deleted file mode 100644 index 04eb595c5..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// 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 EmptyTransitions = new Dictionary(); - - public IReadOnlyDictionary Transitions { get; } - - public string Color { get; } - - public bool NoUpdate { get; } - - public WorkflowStep(IReadOnlyDictionary transitions = null, string color = null, bool noUpdate = false) - { - Transitions = transitions ?? EmptyTransitions; - - Color = color; - - NoUpdate = noUpdate; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs deleted file mode 100644 index 6466ece7a..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// 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 Roles { get; } - - public WorkflowTransition(string expression = null, ReadOnlyCollection roles = null) - { - Expression = expression; - - Roles = roles; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs deleted file mode 100644 index 353323008..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ========================================================================== -// 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 - { - public static readonly Workflows Empty = new Workflows(); - - private Workflows() - { - } - - public Workflows(KeyValuePair[] 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, nameof(name)); - - return new Workflows(With(workflowId, Workflow.CreateDefault(name))); - } - - [Pure] - public Workflows Set(Workflow workflow) - { - Guard.NotNull(workflow, nameof(workflow)); - - return new Workflows(With(Guid.Empty, workflow)); - } - - [Pure] - public Workflows Set(Guid id, Workflow workflow) - { - Guard.NotNull(workflow, nameof(workflow)); - - return new Workflows(With(id, workflow)); - } - - [Pure] - public Workflows Update(Guid id, Workflow workflow) - { - Guard.NotNull(workflow, nameof(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; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs deleted file mode 100644 index a50e9beff..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// 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.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 IFieldPartitionItem.Fallback - { - get { return Enumerable.Empty(); } - } - - private InvariantPartitioning() - { - } - - public bool TryGetItem(string key, out IFieldPartitionItem item) - { - var isFound = string.Equals(key, Key, StringComparison.OrdinalIgnoreCase); - - item = isFound ? this : null; - - return isFound; - } - - IEnumerator IEnumerable.GetEnumerator() - { - yield return this; - } - - IEnumerator IEnumerable.GetEnumerator() - { - yield return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Named.cs b/src/Squidex.Domain.Apps.Core.Model/Named.cs deleted file mode 100644 index fd76c4e8f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Named.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// 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, nameof(name)); - - Name = name; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs deleted file mode 100644 index 8190674f1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core -{ - public delegate IFieldPartitioning PartitionResolver(Partitioning key); - - public sealed class Partitioning : IEquatable - { - public static readonly Partitioning Invariant = new Partitioning("invariant"); - public static readonly Partitioning Language = new Partitioning("language"); - - public string Key { get; } - - public Partitioning(string key) - { - Guard.NotNullOrEmpty(key, nameof(key)); - - Key = key; - } - - public override bool Equals(object obj) - { - return Equals(obj as Partitioning); - } - - public bool Equals(Partitioning other) - { - return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase); - } - - public override int GetHashCode() - { - return Key.GetHashCode(); - } - - public override string ToString() - { - return Key; - } - - public static Partitioning FromString(string value) - { - var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase); - - return isLanguage ? Language : Invariant; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs deleted file mode 100644 index 089fc0ae6..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Core -{ - public static class PartitioningExtensions - { - private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - Partitioning.Language.Key, - Partitioning.Invariant.Key - }; - - public static bool IsValidPartitioning(this string value) - { - return value == null || AllowedPartitions.Contains(value); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs deleted file mode 100644 index 7517186a4..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Rules -{ - public sealed class Rule : Cloneable - { - private RuleTrigger trigger; - private RuleAction action; - private string name; - private bool isEnabled = true; - - public string Name - { - get { return name; } - } - - public RuleTrigger Trigger - { - get { return trigger; } - } - - public RuleAction Action - { - get { return action; } - } - - public bool IsEnabled - { - get { return isEnabled; } - } - - public Rule(RuleTrigger trigger, RuleAction action) - { - Guard.NotNull(trigger, nameof(trigger)); - Guard.NotNull(action, nameof(action)); - - this.trigger = trigger; - this.trigger.Freeze(); - - this.action = action; - this.action.Freeze(); - } - - [Pure] - public Rule Rename(string name) - { - return Clone(clone => - { - clone.name = name; - }); - } - - [Pure] - public Rule Enable() - { - return Clone(clone => - { - clone.isEnabled = true; - }); - } - - [Pure] - public Rule Disable() - { - return Clone(clone => - { - clone.isEnabled = false; - }); - } - - [Pure] - public Rule Update(RuleTrigger newTrigger) - { - Guard.NotNull(newTrigger, nameof(newTrigger)); - - if (newTrigger.GetType() != trigger.GetType()) - { - throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); - } - - newTrigger.Freeze(); - - return Clone(clone => - { - clone.trigger = newTrigger; - }); - } - - [Pure] - public Rule Update(RuleAction newAction) - { - Guard.NotNull(newAction, nameof(newAction)); - - if (newAction.GetType() != action.GetType()) - { - throw new ArgumentException("New action has another type.", nameof(newAction)); - } - - newAction.Freeze(); - - return Clone(clone => - { - clone.action = newAction; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs deleted file mode 100644 index 76086d174..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Rules.Triggers -{ - public sealed class ContentChangedTriggerSchemaV2 : Freezable - { - public Guid SchemaId { get; set; } - - public string Condition { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs deleted file mode 100644 index 77cf55f72..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ========================================================================== -// 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 Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class ArrayField : RootField, IArrayField - { - private FieldCollection fields = FieldCollection.Empty; - - public IReadOnlyList Fields - { - get { return fields.Ordered; } - } - - public IReadOnlyDictionary FieldsById - { - get { return fields.ById; } - } - - public IReadOnlyDictionary FieldsByName - { - get { return fields.ByName; } - } - - public FieldCollection FieldCollection - { - get { return fields; } - } - - public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) - : base(id, name, partitioning, properties, settings) - { - } - - public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties properties = null, IFieldSettings settings = null) - : this(id, name, partitioning, properties, settings) - { - Guard.NotNull(fields, nameof(fields)); - - this.fields = new FieldCollection(fields); - } - - [Pure] - public ArrayField DeleteField(long fieldId) - { - return Updatefields(f => f.Remove(fieldId)); - } - - [Pure] - public ArrayField ReorderFields(List ids) - { - return Updatefields(f => f.Reorder(ids)); - } - - [Pure] - public ArrayField AddField(NestedField field) - { - return Updatefields(f => f.Add(field)); - } - - [Pure] - public ArrayField UpdateField(long fieldId, Func updater) - { - return Updatefields(f => f.Update(fieldId, updater)); - } - - private ArrayField Updatefields(Func, FieldCollection> updater) - { - var newFields = updater(fields); - - if (ReferenceEquals(newFields, fields)) - { - return this; - } - - return Clone(clone => - { - clone.fields = newFields; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs deleted file mode 100644 index 27ee0a32f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class ArrayFieldProperties : FieldProperties - { - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IArrayField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Array(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs deleted file mode 100644 index 5bdb42606..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// 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.Schemas -{ - public sealed class AssetsFieldProperties : FieldProperties - { - public bool MustBeImage { get; set; } - - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public int? MinWidth { get; set; } - - public int? MaxWidth { get; set; } - - public int? MinHeight { get; set; } - - public int? MaxHeight { get; set; } - - public int? MinSize { get; set; } - - public int? MaxSize { get; set; } - - public int? AspectWidth { get; set; } - - public int? AspectHeight { get; set; } - - public bool AllowDuplicates { get; set; } - - public bool ResolveImage { get; set; } - - public ReadOnlyCollection AllowedExtensions { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Assets(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Assets(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs deleted file mode 100644 index abf76f41d..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class BooleanFieldProperties : FieldProperties - { - public bool? DefaultValue { get; set; } - - public bool InlineEditable { get; set; } - - public BooleanFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Boolean(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Boolean(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs deleted file mode 100644 index ca80bf2f4..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class DateTimeFieldProperties : FieldProperties - { - public Instant? MaxValue { get; set; } - - public Instant? MinValue { get; set; } - - public Instant? DefaultValue { get; set; } - - public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } - - public DateTimeFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.DateTime(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.DateTime(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs deleted file mode 100644 index 4450ef2d1..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// 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; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class FieldCollection : Cloneable> where T : IField - { - public static readonly FieldCollection Empty = new FieldCollection(); - - private static readonly Dictionary EmptyById = new Dictionary(); - private static readonly Dictionary EmptyByString = new Dictionary(); - - private T[] fieldsOrdered; - private Dictionary fieldsById; - private Dictionary fieldsByName; - - public IReadOnlyList Ordered - { - get { return fieldsOrdered; } - } - - public IReadOnlyDictionary ById - { - get - { - if (fieldsById == null) - { - if (fieldsOrdered.Length == 0) - { - fieldsById = EmptyById; - } - else - { - fieldsById = fieldsOrdered.ToDictionary(x => x.Id); - } - } - - return fieldsById; - } - } - - public IReadOnlyDictionary ByName - { - get - { - if (fieldsByName == null) - { - if (fieldsOrdered.Length == 0) - { - fieldsByName = EmptyByString; - } - else - { - fieldsByName = fieldsOrdered.ToDictionary(x => x.Name); - } - } - - return fieldsByName; - } - } - - private FieldCollection() - { - fieldsOrdered = Array.Empty(); - } - - public FieldCollection(T[] fields) - { - Guard.NotNull(fields, nameof(fields)); - - fieldsOrdered = fields; - } - - protected override void OnCloned() - { - fieldsById = null; - fieldsByName = null; - } - - [Pure] - public FieldCollection Remove(long fieldId) - { - if (!ById.TryGetValue(fieldId, out _)) - { - return this; - } - - return Clone(clone => - { - clone.fieldsOrdered = fieldsOrdered.Where(x => x.Id != fieldId).ToArray(); - }); - } - - [Pure] - public FieldCollection Reorder(List ids) - { - Guard.NotNull(ids, nameof(ids)); - - if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x))) - { - throw new ArgumentException("Ids must cover all fields.", nameof(ids)); - } - - return Clone(clone => - { - clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray(); - }); - } - - [Pure] - public FieldCollection Add(T field) - { - Guard.NotNull(field, nameof(field)); - - if (ByName.ContainsKey(field.Name)) - { - throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); - } - - if (ById.ContainsKey(field.Id)) - { - throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field)); - } - - return Clone(clone => - { - clone.fieldsOrdered = clone.fieldsOrdered.Union(Enumerable.Repeat(field, 1)).ToArray(); - }); - } - - [Pure] - public FieldCollection Update(long fieldId, Func updater) - { - Guard.NotNull(updater, nameof(updater)); - - if (!ById.TryGetValue(fieldId, out var field)) - { - return this; - } - - var newField = updater(field); - - if (ReferenceEquals(newField, field)) - { - return this; - } - - if (!(newField is T)) - { - throw new InvalidOperationException($"Field must be of type {typeof(T)}"); - } - - return Clone(clone => - { - clone.fieldsOrdered = clone.fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x).ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs deleted file mode 100644 index 36ae3e210..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// 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.Schemas -{ - public abstract class FieldProperties : NamedElementPropertiesBase - { - public bool IsRequired { get; set; } - - public bool IsListField { get; set; } - - public bool IsReferenceField { get; set; } - - public string Placeholder { get; set; } - - public string EditorUrl { get; set; } - - public ReadOnlyCollection Tags { get; set; } - - public abstract T Accept(IFieldPropertiesVisitor visitor); - - public abstract T Accept(IFieldVisitor visitor, IField field); - - public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null); - - public abstract NestedField CreateNestedField(long id, string name, IFieldSettings settings = null); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs deleted file mode 100644 index 1938ad663..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ /dev/null @@ -1,236 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public static class Fields - { - public static RootField Array(long id, string name, Partitioning partitioning, params NestedField[] fields) - { - return new ArrayField(id, name, partitioning, fields); - } - - public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) - { - return new ArrayField(id, name, partitioning, properties, settings); - } - - public static RootField Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Json(long id, string name, Partitioning partitioning, JsonFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Number(long id, string name, Partitioning partitioning, NumberFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField References(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField String(long id, string name, Partitioning partitioning, StringFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Tags(long id, string name, Partitioning partitioning, TagsFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField UI(long id, string name, Partitioning partitioning, UIFieldProperties properties = null, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static NestedField Assets(long id, string name, AssetsFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Boolean(long id, string name, BooleanFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField DateTime(long id, string name, DateTimeFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Geolocation(long id, string name, GeolocationFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Json(long id, string name, JsonFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Number(long id, string name, NumberFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField References(long id, string name, ReferencesFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField String(long id, string name, StringFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField Tags(long id, string name, TagsFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static NestedField UI(long id, string name, UIFieldProperties properties = null, IFieldSettings settings = null) - { - return new NestedField(id, name, properties, settings); - } - - public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func handler = null, ArrayFieldProperties properties = null, IFieldSettings settings = null) - { - var field = Array(id, name, partitioning, properties, settings); - - if (handler != null) - { - field = handler(field); - } - - return schema.AddField(field); - } - - public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(Assets(id, name, partitioning, properties, settings)); - } - - public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(Boolean(id, name, partitioning, properties, settings)); - } - - public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(DateTime(id, name, partitioning, properties, settings)); - } - - public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(Geolocation(id, name, partitioning, properties, settings)); - } - - public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning, JsonFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(Json(id, name, partitioning, properties, settings)); - } - - public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning, NumberFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(Number(id, name, partitioning, properties, settings)); - } - - public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(References(id, name, partitioning, properties, settings)); - } - - public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning, StringFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(String(id, name, partitioning, properties, settings)); - } - - public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning, TagsFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(Tags(id, name, partitioning, properties, settings)); - } - - public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning, UIFieldProperties properties = null, IFieldSettings settings = null) - { - return schema.AddField(UI(id, name, partitioning, properties, settings)); - } - - public static ArrayField AddAssets(this ArrayField field, long id, string name, AssetsFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(Assets(id, name, properties, settings)); - } - - public static ArrayField AddBoolean(this ArrayField field, long id, string name, BooleanFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(Boolean(id, name, properties, settings)); - } - - public static ArrayField AddDateTime(this ArrayField field, long id, string name, DateTimeFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(DateTime(id, name, properties, settings)); - } - - public static ArrayField AddGeolocation(this ArrayField field, long id, string name, GeolocationFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(Geolocation(id, name, properties, settings)); - } - - public static ArrayField AddJson(this ArrayField field, long id, string name, JsonFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(Json(id, name, properties, settings)); - } - - public static ArrayField AddNumber(this ArrayField field, long id, string name, NumberFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(Number(id, name, properties, settings)); - } - - public static ArrayField AddReferences(this ArrayField field, long id, string name, ReferencesFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(References(id, name, properties, settings)); - } - - public static ArrayField AddString(this ArrayField field, long id, string name, StringFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(String(id, name, properties, settings)); - } - - public static ArrayField AddTags(this ArrayField field, long id, string name, TagsFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(Tags(id, name, properties, settings)); - } - - public static ArrayField AddUI(this ArrayField field, long id, string name, UIFieldProperties properties = null, IFieldSettings settings = null) - { - return field.AddField(UI(id, name, properties, settings)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs deleted file mode 100644 index b6f0a9804..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class GeolocationFieldProperties : FieldProperties - { - public GeolocationFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Geolocation(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Geolocation(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs deleted file mode 100644 index 3a7a90900..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Squidex.Infrastructure; -using P = Squidex.Domain.Apps.Core.Partitioning; - -namespace Squidex.Domain.Apps.Core.Schemas.Json -{ - public sealed class JsonFieldModel : IFieldSettings - { - [JsonProperty] - public long Id { get; set; } - - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Partitioning { get; set; } - - [JsonProperty] - public bool IsHidden { get; set; } - - [JsonProperty] - public bool IsLocked { get; set; } - - [JsonProperty] - public bool IsDisabled { get; set; } - - [JsonProperty] - public FieldProperties Properties { get; set; } - - [JsonProperty] - public JsonNestedFieldModel[] Children { get; set; } - - public RootField ToField() - { - var partitioning = P.FromString(Partitioning); - - if (Properties is ArrayFieldProperties arrayProperties) - { - var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty(); - - return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); - } - else - { - return Properties.CreateRootField(Id, Name, partitioning, this); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs deleted file mode 100644 index 54c31c88f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs +++ /dev/null @@ -1,111 +0,0 @@ -// ========================================================================== -// 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 Newtonsoft.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Core.Schemas.Json -{ - public sealed class JsonSchemaModel - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Category { get; set; } - - [JsonProperty] - public bool IsSingleton { get; set; } - - [JsonProperty] - public bool IsPublished { get; set; } - - [JsonProperty] - public SchemaProperties Properties { get; set; } - - [JsonProperty] - public SchemaScripts Scripts { get; set; } - - [JsonProperty] - public JsonFieldModel[] Fields { get; set; } - - [JsonProperty] - public Dictionary PreviewUrls { get; set; } - - public JsonSchemaModel() - { - } - - public JsonSchemaModel(Schema schema) - { - SimpleMapper.Map(schema, this); - - Fields = - schema.Fields.Select(x => - new JsonFieldModel - { - Id = x.Id, - Name = x.Name, - Children = CreateChildren(x), - IsHidden = x.IsHidden, - IsLocked = x.IsLocked, - IsDisabled = x.IsDisabled, - Partitioning = x.Partitioning.Key, - Properties = x.RawProperties - }).ToArray(); - - PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value); - } - - private static JsonNestedFieldModel[] CreateChildren(IField field) - { - if (field is ArrayField arrayField) - { - return arrayField.Fields.Select(x => - new JsonNestedFieldModel - { - Id = x.Id, - Name = x.Name, - IsHidden = x.IsHidden, - IsLocked = x.IsLocked, - IsDisabled = x.IsDisabled, - Properties = x.RawProperties - }).ToArray(); - } - - return null; - } - - public Schema ToSchema() - { - var fields = Fields.Map(f => f.ToField()) ?? Array.Empty(); - - var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton); - - if (!string.IsNullOrWhiteSpace(Category)) - { - schema = schema.ChangeCategory(Category); - } - - if (Scripts != null) - { - schema = schema.ConfigureScripts(Scripts); - } - - if (PreviewUrls?.Count > 0) - { - schema = schema.ConfigurePreviewUrls(PreviewUrls); - } - - return schema; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs deleted file mode 100644 index 5dc24c564..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class JsonFieldProperties : FieldProperties - { - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Json(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Json(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs deleted file mode 100644 index 9b3b92aba..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class NamedElementPropertiesBase : Freezable - { - public string Label { get; set; } - - public string Hints { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs deleted file mode 100644 index 9643c2ea4..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class NestedField : Cloneable, INestedField - { - private readonly long fieldId; - private readonly string fieldName; - private bool isDisabled; - private bool isHidden; - private bool isLocked; - - public long Id - { - get { return fieldId; } - } - - public string Name - { - get { return fieldName; } - } - - public bool IsLocked - { - get { return isLocked; } - } - - public bool IsHidden - { - get { return isHidden; } - } - - public bool IsDisabled - { - get { return isDisabled; } - } - - public abstract FieldProperties RawProperties { get; } - - protected NestedField(long id, string name, IFieldSettings settings = null) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.GreaterThan(id, 0, nameof(id)); - - fieldId = id; - fieldName = name; - - if (settings != null) - { - isLocked = settings.IsLocked; - isHidden = settings.IsHidden; - isDisabled = settings.IsDisabled; - } - } - - [Pure] - public NestedField Lock() - { - return Clone(clone => - { - clone.isLocked = true; - }); - } - - [Pure] - public NestedField Hide() - { - return Clone(clone => - { - clone.isHidden = true; - }); - } - - [Pure] - public NestedField Show() - { - return Clone(clone => - { - clone.isHidden = false; - }); - } - - [Pure] - public NestedField Disable() - { - return Clone(clone => - { - clone.isDisabled = true; - }); - } - - [Pure] - public NestedField Enable() - { - return Clone(clone => - { - clone.isDisabled = false; - }); - } - - public abstract T Accept(IFieldVisitor visitor); - - public abstract NestedField Update(FieldProperties newProperties); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs deleted file mode 100644 index 808de167b..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class NestedField : NestedField, IField where T : FieldProperties, new() - { - private T properties; - - public T Properties - { - get { return properties; } - } - - public override FieldProperties RawProperties - { - get { return properties; } - } - - public NestedField(long id, string name, T properties = null, IFieldSettings settings = null) - : base(id, name, settings) - { - SetProperties(properties ?? new T()); - } - - [Pure] - public override NestedField Update(FieldProperties newProperties) - { - var typedProperties = ValidateProperties(newProperties); - - return Clone>(clone => - { - clone.SetProperties(typedProperties); - }); - } - - private void SetProperties(T newProperties) - { - properties = newProperties; - properties.Freeze(); - } - - private T ValidateProperties(FieldProperties newProperties) - { - Guard.NotNull(newProperties, nameof(newProperties)); - - if (!(newProperties is T typedProperties)) - { - throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); - } - - return typedProperties; - } - - public override TResult Accept(IFieldVisitor visitor) - { - return properties.Accept(visitor, this); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs deleted file mode 100644 index dedbe213f..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// 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.Schemas -{ - public sealed class NumberFieldProperties : FieldProperties - { - public ReadOnlyCollection AllowedValues { get; set; } - - public double? MaxValue { get; set; } - - public double? MinValue { get; set; } - - public double? DefaultValue { get; set; } - - public bool IsUnique { get; set; } - - public bool InlineEditable { get; set; } - - public NumberFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Number(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Number(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs deleted file mode 100644 index bca51a0cc..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class ReferencesFieldProperties : FieldProperties - { - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public bool ResolveReference { get; set; } - - public bool AllowDuplicates { get; set; } - - public ReferencesFieldEditor Editor { get; set; } - - public ReadOnlyCollection SchemaIds { get; set; } - - public Guid SchemaId - { - set - { - if (value != default) - { - SchemaIds = new ReadOnlyCollection(new List { value }); - } - else - { - SchemaIds = null; - } - } - } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.References(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.References(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs deleted file mode 100644 index 6c21a1054..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public abstract class RootField : Cloneable, IRootField - { - private readonly long fieldId; - private readonly string fieldName; - private readonly Partitioning partitioning; - private bool isDisabled; - private bool isHidden; - private bool isLocked; - - public long Id - { - get { return fieldId; } - } - - public string Name - { - get { return fieldName; } - } - - public bool IsLocked - { - get { return isLocked; } - } - - public bool IsHidden - { - get { return isHidden; } - } - - public bool IsDisabled - { - get { return isDisabled; } - } - - public Partitioning Partitioning - { - get { return partitioning; } - } - - public abstract FieldProperties RawProperties { get; } - - protected RootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.GreaterThan(id, 0, nameof(id)); - Guard.NotNull(partitioning, nameof(partitioning)); - - fieldId = id; - fieldName = name; - - this.partitioning = partitioning; - - if (settings != null) - { - isLocked = settings.IsLocked; - isHidden = settings.IsHidden; - isDisabled = settings.IsDisabled; - } - } - - [Pure] - public RootField Lock() - { - return Clone(clone => - { - clone.isLocked = true; - }); - } - - [Pure] - public RootField Hide() - { - return Clone(clone => - { - clone.isHidden = true; - }); - } - - [Pure] - public RootField Show() - { - return Clone(clone => - { - clone.isHidden = false; - }); - } - - [Pure] - public RootField Disable() - { - return Clone(clone => - { - clone.isDisabled = true; - }); - } - - [Pure] - public RootField Enable() - { - return Clone(clone => - { - clone.isDisabled = false; - }); - } - - public abstract T Accept(IFieldVisitor visitor); - - public abstract RootField Update(FieldProperties newProperties); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs deleted file mode 100644 index cbe6716d0..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics.Contracts; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class RootField : RootField, IField where T : FieldProperties, new() - { - private T properties; - - public T Properties - { - get { return properties; } - } - - public override FieldProperties RawProperties - { - get { return properties; } - } - - public RootField(long id, string name, Partitioning partitioning, T properties = null, IFieldSettings settings = null) - : base(id, name, partitioning, settings) - { - SetProperties(properties ?? new T()); - } - - [Pure] - public override RootField Update(FieldProperties newProperties) - { - var typedProperties = ValidateProperties(newProperties); - - return Clone>(clone => - { - clone.SetProperties(typedProperties); - }); - } - - private void SetProperties(T newProperties) - { - properties = newProperties; - properties.Freeze(); - } - - private T ValidateProperties(FieldProperties newProperties) - { - Guard.NotNull(newProperties, nameof(newProperties)); - - if (!(newProperties is T typedProperties)) - { - throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); - } - - return typedProperties; - } - - public override TResult Accept(IFieldVisitor visitor) - { - return properties.Accept(visitor, this); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs deleted file mode 100644 index 7b188283a..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ /dev/null @@ -1,201 +0,0 @@ -// ========================================================================== -// 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; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class Schema : Cloneable - { - private static readonly Dictionary EmptyPreviewUrls = new Dictionary(); - private readonly string name; - private readonly bool isSingleton; - private string category; - private FieldCollection fields = FieldCollection.Empty; - private IReadOnlyDictionary previewUrls = EmptyPreviewUrls; - private SchemaScripts scripts = new SchemaScripts(); - private SchemaProperties properties; - private bool isPublished; - - public string Name - { - get { return name; } - } - - public string Category - { - get { return category; } - } - - public bool IsPublished - { - get { return isPublished; } - } - - public bool IsSingleton - { - get { return isSingleton; } - } - - public IReadOnlyList Fields - { - get { return fields.Ordered; } - } - - public IReadOnlyDictionary FieldsById - { - get { return fields.ById; } - } - - public IReadOnlyDictionary FieldsByName - { - get { return fields.ByName; } - } - - public IReadOnlyDictionary PreviewUrls - { - get { return previewUrls; } - } - - public FieldCollection FieldCollection - { - get { return fields; } - } - - public SchemaScripts Scripts - { - get { return scripts; } - } - - public SchemaProperties Properties - { - get { return properties; } - } - - public Schema(string name, SchemaProperties properties = null, bool isSingleton = false) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - this.name = name; - - this.properties = properties ?? new SchemaProperties(); - this.properties.Freeze(); - - this.isSingleton = isSingleton; - } - - public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished, bool isSingleton = false) - : this(name, properties, isSingleton) - { - Guard.NotNull(fields, nameof(fields)); - - this.fields = new FieldCollection(fields); - - this.isPublished = isPublished; - } - - [Pure] - public Schema Update(SchemaProperties newProperties) - { - Guard.NotNull(newProperties, nameof(newProperties)); - - return Clone(clone => - { - clone.properties = newProperties; - clone.properties.Freeze(); - }); - } - - [Pure] - public Schema ConfigureScripts(SchemaScripts newScripts) - { - return Clone(clone => - { - clone.scripts = newScripts ?? new SchemaScripts(); - clone.scripts.Freeze(); - }); - } - - [Pure] - public Schema Publish() - { - return Clone(clone => - { - clone.isPublished = true; - }); - } - - [Pure] - public Schema Unpublish() - { - return Clone(clone => - { - clone.isPublished = false; - }); - } - - [Pure] - public Schema ChangeCategory(string newCategory) - { - return Clone(clone => - { - clone.category = newCategory; - }); - } - - [Pure] - public Schema ConfigurePreviewUrls(IReadOnlyDictionary newPreviewUrls) - { - return Clone(clone => - { - clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls; - }); - } - - [Pure] - public Schema DeleteField(long fieldId) - { - return UpdateFields(f => f.Remove(fieldId)); - } - - [Pure] - public Schema ReorderFields(List ids) - { - return UpdateFields(f => f.Reorder(ids)); - } - - [Pure] - public Schema AddField(RootField field) - { - return UpdateFields(f => f.Add(field)); - } - - [Pure] - public Schema UpdateField(long fieldId, Func updater) - { - return UpdateFields(f => f.Update(fieldId, updater)); - } - - private Schema UpdateFields(Func, FieldCollection> updater) - { - var newFields = updater(fields); - - if (ReferenceEquals(newFields, fields)) - { - return this; - } - - return Clone(clone => - { - clone.fields = newFields; - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs deleted file mode 100644 index 1b28964ac..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// 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.Schemas -{ - public sealed class StringFieldProperties : FieldProperties - { - public ReadOnlyCollection AllowedValues { get; set; } - - public int? MinLength { get; set; } - - public int? MaxLength { get; set; } - - public bool IsUnique { get; set; } - - public bool InlineEditable { get; set; } - - public string DefaultValue { get; set; } - - public string Pattern { get; set; } - - public string PatternMessage { get; set; } - - public StringFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.String(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.String(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs deleted file mode 100644 index d81043aba..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// 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.Schemas -{ - public sealed class TagsFieldProperties : FieldProperties - { - public ReadOnlyCollection AllowedValues { get; set; } - - public int? MinItems { get; set; } - - public int? MaxItems { get; set; } - - public TagsFieldEditor Editor { get; set; } - - public TagsFieldNormalization Normalization { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return Fields.Tags(id, name, partitioning, this, settings); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return Fields.Tags(id, name, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs deleted file mode 100644 index 3fd109ce8..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public sealed class UIFieldProperties : FieldProperties - { - public UIFieldEditor Editor { get; set; } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return visitor.Visit(this); - } - - public override T Accept(IFieldVisitor visitor, IField field) - { - return visitor.Visit((IField)field); - } - - public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) - { - return new NestedField(id, name, this, settings); - } - - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) - { - return new RootField(id, name, partitioning, this, settings); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj deleted file mode 100644 index 3dc19b23e..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - netstandard2.0 - Squidex.Domain.Apps.Core - 7.3 - - - full - True - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs deleted file mode 100644 index 8a9ba47cc..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// 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 System.Text; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ConvertContent -{ - public static class ContentConverter - { - private static readonly Func KeyNameResolver = f => f.Name; - private static readonly Func KeyIdResolver = f => f.Id; - - public static string ToFullText(this ContentData data, int maxTotalLength = 1024 * 1024, int maxFieldLength = 1000, string separator = " ") - { - var stringBuilder = new StringBuilder(); - - foreach (var value in data.Values.SelectMany(x => x.Values)) - { - AppendText(value, stringBuilder, maxFieldLength, separator, false); - } - - var result = stringBuilder.ToString(); - - if (result.Length > maxTotalLength) - { - result = result.Substring(0, maxTotalLength); - } - - return result; - } - - private static void AppendText(IJsonValue value, StringBuilder stringBuilder, int maxFieldLength, string separator, bool allowObjects) - { - if (value.Type == JsonValueType.String) - { - var text = value.ToString(); - - if (text.Length <= maxFieldLength) - { - if (stringBuilder.Length > 0) - { - stringBuilder.Append(separator); - } - - stringBuilder.Append(text); - } - } - else if (value is JsonArray array) - { - foreach (var item in array) - { - AppendText(item, stringBuilder, maxFieldLength, separator, true); - } - } - else if (value is JsonObject obj && allowObjects) - { - foreach (var item in obj.Values) - { - AppendText(item, stringBuilder, maxFieldLength, separator, true); - } - } - } - - public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new NamedContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsById, KeyNameResolver, converters); - } - - public static IdContentData ConvertId2Id(this IdContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new IdContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters); - } - - public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new NamedContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsByName, KeyNameResolver, converters); - } - - public static IdContentData ConvertName2Id(this NamedContentData content, Schema schema, params FieldConverter[] converters) - { - Guard.NotNull(schema, nameof(schema)); - - var result = new IdContentData(content.Count); - - return ConvertInternal(content, result, schema.FieldsByName, KeyIdResolver, converters); - } - - private static TDict2 ConvertInternal( - TDict1 source, - TDict2 target, - IReadOnlyDictionary fields, - Func targetKey, params FieldConverter[] converters) - where TDict1 : IDictionary - where TDict2 : IDictionary - { - foreach (var fieldKvp in source) - { - if (!fields.TryGetValue(fieldKvp.Key, out var field)) - { - continue; - } - - var newvalue = fieldKvp.Value; - - if (converters != null) - { - foreach (var converter in converters) - { - newvalue = converter(newvalue, field); - - if (newvalue == null) - { - break; - } - } - } - - if (newvalue != null) - { - target.Add(targetKey(field), newvalue); - } - } - - return target; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs deleted file mode 100644 index 8a6e82895..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ConvertContent -{ - public static class ContentConverterFlat - { - public static object ToFlatLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection languagePreferences = null) - { - Guard.NotNull(languagesConfig, nameof(languagesConfig)); - - if (languagePreferences == null || languagePreferences.Count == 0) - { - return content; - } - - if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) - { - languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); - } - - var result = new Dictionary(); - - foreach (var fieldValue in content) - { - var fieldData = fieldValue.Value; - - foreach (var language in languagePreferences) - { - if (fieldData.TryGetValue(language, out var value) && value.Type != JsonValueType.Null) - { - result[fieldValue.Key] = value; - - break; - } - } - } - - return result; - } - - public static Dictionary ToFlatten(this NamedContentData content) - { - var result = new Dictionary(); - - foreach (var fieldValue in content) - { - var fieldData = fieldValue.Value; - - if (fieldData.Count == 1) - { - result[fieldValue.Key] = fieldData.Values.First(); - } - else - { - result[fieldValue.Key] = fieldData; - } - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs deleted file mode 100644 index f9e35bb58..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ /dev/null @@ -1,373 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -#pragma warning disable RECS0002 // Convert anonymous method to method group - -namespace Squidex.Domain.Apps.Core.ConvertContent -{ - public delegate ContentFieldData FieldConverter(ContentFieldData data, IRootField field); - - public static class FieldConverters - { - private static readonly Func KeyNameResolver = f => f.Name; - private static readonly Func KeyIdResolver = f => f.Id.ToString(); - - private static readonly Func FieldByIdResolver = - (f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null; - - private static readonly Func FieldByNameResolver = - (f, k) => f.FieldsByName.GetOrDefault(k); - - public static FieldConverter ExcludeHidden() - { - return (data, field) => !field.IsForApi() ? null : data; - } - - public static FieldConverter ExcludeChangedTypes() - { - return (data, field) => - { - foreach (var value in data.Values) - { - if (value.Type == JsonValueType.Null) - { - continue; - } - - try - { - JsonValueConverter.ConvertValue(field, value); - } - catch - { - return null; - } - } - - return data; - }; - } - - public static FieldConverter ResolveAssetUrls(IReadOnlyCollection fields, IAssetUrlGenerator urlGenerator) - { - if (fields?.Any() != true) - { - return (data, field) => data; - } - - var isAll = fields.First() == "*"; - - return (data, field) => - { - if (field is IField && (isAll || fields.Contains(field.Name))) - { - foreach (var partition in data) - { - if (partition.Value is JsonArray array) - { - for (var i = 0; i < array.Count; i++) - { - var id = array[i].ToString(); - - array[i] = JsonValue.Create(urlGenerator.GenerateUrl(id)); - } - } - } - } - - return data; - }; - } - - public static FieldConverter ResolveInvariant(LanguagesConfig config) - { - var codeForInvariant = InvariantPartitioning.Key; - var codeForMasterLanguage = config.Master.Language.Iso2Code; - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Invariant)) - { - var result = new ContentFieldData(); - - if (data.TryGetValue(codeForInvariant, out var value)) - { - result[codeForInvariant] = value; - } - else if (data.TryGetValue(codeForMasterLanguage, out value)) - { - result[codeForInvariant] = value; - } - else if (data.Count > 0) - { - result[codeForInvariant] = data.Values.First(); - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ResolveLanguages(LanguagesConfig config) - { - var codeForInvariant = InvariantPartitioning.Key; - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Language)) - { - var result = new ContentFieldData(); - - foreach (var languageConfig in config) - { - var languageCode = languageConfig.Key; - - if (data.TryGetValue(languageCode, out var value)) - { - result[languageCode] = value; - } - else if (languageConfig == config.Master && data.TryGetValue(codeForInvariant, out value)) - { - result[languageCode] = value; - } - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ResolveFallbackLanguages(LanguagesConfig config) - { - var master = config.Master; - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Language)) - { - foreach (var languageConfig in config) - { - var languageCode = languageConfig.Key; - - if (!data.TryGetValue(languageCode, out var value)) - { - var dataFound = false; - - foreach (var fallback in languageConfig.Fallback) - { - if (data.TryGetValue(fallback, out value)) - { - data[languageCode] = value; - dataFound = true; - break; - } - } - - if (!dataFound && languageConfig != master) - { - if (data.TryGetValue(master.Language, out value)) - { - data[languageCode] = value; - } - } - } - } - } - - return data; - }; - } - - public static FieldConverter FilterLanguages(LanguagesConfig config, IEnumerable languages) - { - if (languages?.Any() != true) - { - return (data, field) => data; - } - - var languageSet = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var language in languages) - { - if (config.Contains(language.Iso2Code)) - { - languageSet.Add(language.Iso2Code); - } - } - - if (languageSet.Count == 0) - { - languageSet.Add(config.Master.Language.Iso2Code); - } - - return (data, field) => - { - if (field.Partitioning.Equals(Partitioning.Language)) - { - var result = new ContentFieldData(); - - foreach (var languageCode in languageSet) - { - if (data.TryGetValue(languageCode, out var value)) - { - result[languageCode] = value; - } - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ForNestedName2Name(params ValueConverter[] converters) - { - return ForNested(FieldByNameResolver, KeyNameResolver, converters); - } - - public static FieldConverter ForNestedName2Id(params ValueConverter[] converters) - { - return ForNested(FieldByNameResolver, KeyIdResolver, converters); - } - - public static FieldConverter ForNestedId2Name(params ValueConverter[] converters) - { - return ForNested(FieldByIdResolver, KeyNameResolver, converters); - } - - public static FieldConverter ForNestedId2Id(params ValueConverter[] converters) - { - return ForNested(FieldByIdResolver, KeyIdResolver, converters); - } - - private static FieldConverter ForNested( - Func fieldResolver, - Func keyResolver, - params ValueConverter[] converters) - { - return (data, field) => - { - if (field is IArrayField arrayField) - { - var result = new ContentFieldData(); - - foreach (var partition in data) - { - if (!(partition.Value is JsonArray array)) - { - continue; - } - - var newArray = JsonValue.Array(); - - foreach (var item in array.OfType()) - { - var newItem = JsonValue.Object(); - - foreach (var kvp in item) - { - var nestedField = fieldResolver(arrayField, kvp.Key); - - if (nestedField == null) - { - continue; - } - - var newValue = kvp.Value; - - var isUnset = false; - - if (converters != null) - { - foreach (var converter in converters) - { - newValue = converter(newValue, nestedField); - - if (ReferenceEquals(newValue, Value.Unset)) - { - isUnset = true; - break; - } - } - } - - if (!isUnset) - { - newItem.Add(keyResolver(nestedField), newValue); - } - } - - newArray.Add(newItem); - } - - result.Add(partition.Key, newArray); - } - - return result; - } - - return data; - }; - } - - public static FieldConverter ForValues(params ValueConverter[] converters) - { - return (data, field) => - { - if (!(field is IArrayField)) - { - var result = new ContentFieldData(); - - foreach (var partition in data) - { - var newValue = partition.Value; - - var isUnset = false; - - if (converters != null) - { - foreach (var converter in converters) - { - newValue = converter(newValue, field); - - if (ReferenceEquals(newValue, Value.Unset)) - { - isUnset = true; - break; - } - } - } - - if (!isUnset) - { - result.Add(partition.Key, newValue); - } - } - - return result; - } - - return data; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs deleted file mode 100644 index 196225e64..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.EnrichContent -{ - public sealed class ContentEnricher - { - private readonly Schema schema; - private readonly PartitionResolver partitionResolver; - - public ContentEnricher(Schema schema, PartitionResolver partitionResolver) - { - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - this.schema = schema; - - this.partitionResolver = partitionResolver; - } - - public void Enrich(NamedContentData data) - { - Guard.NotNull(data, nameof(data)); - - foreach (var field in schema.Fields) - { - var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); - var fieldPartition = partitionResolver(field.Partitioning); - - foreach (var partitionItem in fieldPartition) - { - Enrich(field, fieldData, partitionItem); - } - - if (fieldData.Count > 0) - { - data[field.Name] = fieldData; - } - } - } - - private static void Enrich(IField field, ContentFieldData fieldData, IFieldPartitionItem partitionItem) - { - Guard.NotNull(fieldData, nameof(fieldData)); - - var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); - - if (field.RawProperties.IsRequired || defaultValue == null || defaultValue.Type == JsonValueType.Null) - { - return; - } - - var key = partitionItem.Key; - - if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value)) - { - fieldData.AddJsonValue(key, defaultValue); - } - } - - private static bool ShouldApplyDefaultValue(IField field, IJsonValue value) - { - return value.Type == JsonValueType.Null || (field is IField && value is JsonScalar s && string.IsNullOrEmpty(s.Value)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs deleted file mode 100644 index 5c3379739..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using NodaTime; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.EnrichContent -{ - public sealed class DefaultValueFactory : IFieldVisitor - { - private readonly Instant now; - - private DefaultValueFactory(Instant now) - { - this.now = now; - } - - public static IJsonValue CreateDefaultValue(IField field, Instant now) - { - Guard.NotNull(field, nameof(field)); - - return field.Accept(new DefaultValueFactory(now)); - } - - public IJsonValue Visit(IArrayField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Create(field.Properties.DefaultValue); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Null; - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Null; - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Create(field.Properties.DefaultValue); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Create(field.Properties.DefaultValue); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Array(); - } - - public IJsonValue Visit(IField field) - { - return JsonValue.Null; - } - - public IJsonValue Visit(IField field) - { - if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) - { - return JsonValue.Create(now.ToString()); - } - - if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) - { - return JsonValue.Create($"{now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); - } - - return JsonValue.Create(field.Properties.DefaultValue?.ToString()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs deleted file mode 100644 index 68e916936..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ /dev/null @@ -1,223 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; - -namespace Squidex.Domain.Apps.Core.EventSynchronization -{ - public static class SchemaSynchronizer - { - public static IEnumerable Synchronize(this Schema source, Schema target, IJsonSerializer serializer, Func idGenerator, SchemaSynchronizationOptions options = null) - { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(idGenerator, nameof(idGenerator)); - - if (target == null) - { - yield return new SchemaDeleted(); - } - else - { - options = options ?? new SchemaSynchronizationOptions(); - - SchemaEvent E(SchemaEvent @event) - { - return @event; - } - - if (!source.Properties.EqualsJson(target.Properties, serializer)) - { - yield return E(new SchemaUpdated { Properties = target.Properties }); - } - - if (!source.Category.StringEquals(target.Category)) - { - yield return E(new SchemaCategoryChanged { Name = target.Category }); - } - - if (!source.Scripts.EqualsJson(target.Scripts, serializer)) - { - yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); - } - - if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) - { - yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary(x => x.Key, x => x.Value) }); - } - - if (source.IsPublished != target.IsPublished) - { - yield return target.IsPublished ? - E(new SchemaPublished()) : - E(new SchemaUnpublished()); - } - - var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, CanUpdateRoot, null, options); - - foreach (var @event in events) - { - yield return E(@event); - } - } - } - - private static IEnumerable SyncFields( - FieldCollection source, - FieldCollection target, - IJsonSerializer serializer, - Func idGenerator, - Func canUpdate, - NamedId parentId, SchemaSynchronizationOptions options) where T : class, IField - { - FieldEvent E(FieldEvent @event) - { - @event.ParentFieldId = parentId; - - return @event; - } - - var sourceIds = new List>(source.Ordered.Select(x => x.NamedId())); - var sourceNames = sourceIds.Select(x => x.Name).ToList(); - - if (!options.NoFieldDeletion) - { - foreach (var sourceField in source.Ordered) - { - if (!target.ByName.TryGetValue(sourceField.Name, out _)) - { - var id = sourceField.NamedId(); - - sourceIds.Remove(id); - sourceNames.Remove(id.Name); - - yield return E(new FieldDeleted { FieldId = id }); - } - } - } - - foreach (var targetField in target.Ordered) - { - NamedId id = null; - - var canCreateField = true; - - if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) - { - canCreateField = false; - - id = sourceField.NamedId(); - - if (canUpdate(sourceField, targetField)) - { - if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer)) - { - yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); - } - } - else if (!sourceField.IsLocked && !options.NoFieldRecreation) - { - canCreateField = true; - - sourceIds.Remove(id); - sourceNames.Remove(id.Name); - - yield return E(new FieldDeleted { FieldId = id }); - } - } - - if (canCreateField) - { - var partitioning = (string)null; - - if (targetField is IRootField rootField) - { - partitioning = rootField.Partitioning.Key; - } - - id = NamedId.Of(idGenerator(), targetField.Name); - - yield return new FieldAdded - { - Name = targetField.Name, - ParentFieldId = parentId, - Partitioning = partitioning, - Properties = targetField.RawProperties, - FieldId = id - }; - - sourceIds.Add(id); - sourceNames.Add(id.Name); - } - - if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) - { - if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) - { - yield return E(new FieldLocked { FieldId = id }); - } - - if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) - { - yield return targetField.IsHidden ? - E(new FieldHidden { FieldId = id }) : - E(new FieldShown { FieldId = id }); - } - - if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) - { - yield return targetField.IsDisabled ? - E(new FieldDisabled { FieldId = id }) : - E(new FieldEnabled { FieldId = id }); - } - - if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) - { - var fields = ((IArrayField)sourceField)?.FieldCollection ?? FieldCollection.Empty; - - var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, CanUpdate, id, options); - - foreach (var @event in events) - { - yield return @event; - } - } - } - } - - if (sourceNames.Count > 1) - { - var targetNames = target.Ordered.Select(x => x.Name); - - if (sourceNames.Intersect(targetNames).Count() == target.Ordered.Count && !sourceNames.SequenceEqual(targetNames)) - { - var fieldIds = targetNames.Select(x => sourceIds.FirstOrDefault(y => y.Name == x).Id).ToList(); - - yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; - } - } - } - - private static bool CanUpdateRoot(IRootField source, IRootField target) - { - return CanUpdate(source, target) && source.Partitioning == target.Partitioning; - } - - private static bool CanUpdate(IField source, IField target) - { - return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs deleted file mode 100644 index f4eeabb58..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// 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 System.Text; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public static class ContentReferencesExtensions - { - public static IEnumerable GetReferencedIds(this IdContentData source, Schema schema, Ids strategy = Ids.All) - { - Guard.NotNull(schema, nameof(schema)); - - foreach (var field in schema.Fields) - { - var ids = source.GetReferencedIds(field, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - - public static IEnumerable GetReferencedIds(this IdContentData source, IField field, Ids strategy = Ids.All) - { - Guard.NotNull(field, nameof(field)); - - if (source.TryGetValue(field.Id, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var ids = field.GetReferencedIds(partitionValue.Value, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - } - - public static IEnumerable GetReferencedIds(this NamedContentData source, Schema schema, Ids strategy = Ids.All) - { - Guard.NotNull(schema, nameof(schema)); - - return GetReferencedIds(source, schema.Fields, strategy); - } - - public static IEnumerable GetReferencedIds(this NamedContentData source, IEnumerable fields, Ids strategy = Ids.All) - { - Guard.NotNull(fields, nameof(fields)); - - foreach (var field in fields) - { - var ids = source.GetReferencedIds(field, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - - public static IEnumerable GetReferencedIds(this NamedContentData source, IField field, Ids strategy = Ids.All) - { - Guard.NotNull(field, nameof(field)); - - if (source.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var ids = field.GetReferencedIds(partitionValue.Value, strategy); - - foreach (var id in ids) - { - yield return id; - } - } - } - } - - public static JsonObject FormatReferences(this NamedContentData data, Schema schema, LanguagesConfig languages, string separator = ", ") - { - Guard.NotNull(schema, nameof(schema)); - - var result = JsonValue.Object(); - - foreach (var language in languages) - { - result[language.Key] = JsonValue.Create(data.FormatReferenceFields(schema, language.Key, separator)); - } - - return result; - } - - private static string FormatReferenceFields(this NamedContentData data, Schema schema, string partition, string separator) - { - Guard.NotNull(schema, nameof(schema)); - - var sb = new StringBuilder(); - - void AddValue(object value) - { - if (sb.Length > 0) - { - sb.Append(separator); - } - - sb.Append(value); - } - - var referenceFields = schema.Fields.Where(x => x.RawProperties.IsReferenceField); - - if (!referenceFields.Any()) - { - referenceFields = schema.Fields.Take(1); - } - - foreach (var referenceField in referenceFields) - { - if (data.TryGetValue(referenceField.Name, out var fieldData)) - { - if (fieldData.TryGetValue(partition, out var value)) - { - AddValue(value); - } - else if (fieldData.TryGetValue(InvariantPartitioning.Key, out var value2)) - { - AddValue(value2); - } - } - } - - return sb.ToString(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs deleted file mode 100644 index 240dffb99..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// 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.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public sealed class ReferencesCleaner : IFieldVisitor - { - private readonly IJsonValue value; - private readonly ICollection oldReferences; - - private ReferencesCleaner(IJsonValue value, ICollection oldReferences) - { - this.value = value; - - this.oldReferences = oldReferences; - } - - public static IJsonValue CleanReferences(IField field, IJsonValue value, ICollection oldReferences) - { - return field.Accept(new ReferencesCleaner(value, oldReferences)); - } - - public IJsonValue Visit(IField field) - { - return CleanIds(); - } - - public IJsonValue Visit(IField field) - { - if (oldReferences.Contains(field.Properties.SingleId())) - { - return JsonValue.Array(); - } - - return CleanIds(); - } - - private IJsonValue CleanIds() - { - var ids = value.ToGuidSet(); - - var isRemoved = false; - - foreach (var oldReference in oldReferences) - { - isRemoved |= ids.Remove(oldReference); - } - - return isRemoved ? ids.ToJsonArray() : value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IField field) - { - return value; - } - - public IJsonValue Visit(IArrayField field) - { - return value; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs deleted file mode 100644 index 47f032123..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public static class ReferencesExtensions - { - public static IEnumerable GetReferencedIds(this IField field, IJsonValue value, Ids strategy = Ids.All) - { - return ReferencesExtractor.ExtractReferences(field, value, strategy); - } - - public static IJsonValue CleanReferences(this IField field, IJsonValue value, ICollection oldReferences) - { - if (IsNull(value)) - { - return value; - } - - return ReferencesCleaner.CleanReferences(field, value, oldReferences); - } - - private static bool IsNull(IJsonValue value) - { - return value == null || value.Type == JsonValueType.Null; - } - - public static JsonArray ToJsonArray(this HashSet ids) - { - var result = JsonValue.Array(); - - foreach (var id in ids) - { - result.Add(JsonValue.Create(id.ToString())); - } - - return result; - } - - public static HashSet ToGuidSet(this IJsonValue value) - { - if (value is JsonArray array) - { - var result = new HashSet(); - - foreach (var id in array) - { - if (id.Type == JsonValueType.String && Guid.TryParse(id.ToString(), out var guid)) - { - result.Add(guid); - } - } - - return result; - } - - return new HashSet(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs deleted file mode 100644 index 90fdb52a6..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// 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.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds -{ - public sealed class ReferencesExtractor : IFieldVisitor> - { - private readonly IJsonValue value; - private readonly Ids strategy; - - private ReferencesExtractor(IJsonValue value, Ids strategy) - { - this.value = value; - - this.strategy = strategy; - } - - public static IEnumerable ExtractReferences(IField field, IJsonValue value, Ids strategy) - { - return field.Accept(new ReferencesExtractor(value, strategy)); - } - - public IEnumerable Visit(IArrayField field) - { - var result = new List(); - - if (value is JsonArray array) - { - foreach (var item in array.OfType()) - { - foreach (var nestedField in field.Fields) - { - if (item.TryGetValue(nestedField.Name, out var nestedValue)) - { - result.AddRange(nestedField.Accept(new ReferencesExtractor(nestedValue, strategy))); - } - } - } - } - - return result; - } - - public IEnumerable Visit(IField field) - { - var ids = value.ToGuidSet(); - - return ids; - } - - public IEnumerable Visit(IField field) - { - var ids = value.ToGuidSet(); - - if (strategy == Ids.All && field.Properties.SchemaIds != null) - { - foreach (var schemaId in field.Properties.SchemaIds) - { - ids.Add(schemaId); - } - } - - return ids; - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - - public IEnumerable Visit(IField field) - { - return Enumerable.Empty(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs deleted file mode 100644 index 55fa21cb7..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.OData.Edm; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateEdmSchema -{ - public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string names); - - public static class EdmSchemaExtensions - { - public static string EscapeEdmField(this string field) - { - return field.Replace("-", "_"); - } - - public static string UnescapeEdmField(this string field) - { - return field.Replace("_", "-"); - } - - public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory) - { - Guard.NotNull(typeFactory, nameof(typeFactory)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - var (edmType, _) = typeFactory("Data"); - - var visitor = new EdmTypeVisitor(typeFactory); - - foreach (var field in schema.FieldsByName.Values) - { - if (!field.IsForApi(withHidden)) - { - continue; - } - - var fieldEdmType = field.Accept(visitor); - - if (fieldEdmType == null) - { - continue; - } - - var (partitionType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}"); - - if (created) - { - var partition = partitionResolver(field.Partitioning); - - foreach (var partitionItem in partition) - { - partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), fieldEdmType); - } - } - - edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); - } - - return edmType; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs deleted file mode 100644 index 1bcabde3a..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.OData.Edm; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateEdmSchema -{ - public sealed class EdmTypeVisitor : IFieldVisitor - { - private readonly EdmTypeFactory typeFactory; - - internal EdmTypeVisitor(EdmTypeFactory typeFactory) - { - this.typeFactory = typeFactory; - } - - public IEdmTypeReference CreateEdmType(IField field) - { - return field.Accept(this); - } - - public IEdmTypeReference Visit(IArrayField field) - { - var (fieldEdmType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}.Item"); - - if (created) - { - foreach (var nestedField in field.Fields) - { - var nestedEdmType = nestedField.Accept(this); - - if (nestedEdmType != null) - { - fieldEdmType.AddStructuralProperty(nestedField.Name.EscapeEdmField(), nestedEdmType); - } - } - } - - return new EdmComplexTypeReference(fieldEdmType, false); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); - } - - public IEdmTypeReference Visit(IField field) - { - return null; - } - - public IEdmTypeReference Visit(IField field) - { - return null; - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.Double, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return CreatePrimitive(EdmPrimitiveTypeKind.String, field); - } - - public IEdmTypeReference Visit(IField field) - { - return null; - } - - private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, IField field) - { - return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs deleted file mode 100644 index f7f5fcd7b..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NJsonSchema; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public static class Builder - { - public static JsonSchema Object() - { - return new JsonSchema { Type = JsonObjectType.Object }; - } - - public static JsonSchema Guid() - { - return new JsonSchema { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }; - } - - public static JsonSchema String() - { - return new JsonSchema { Type = JsonObjectType.String }; - } - - public static JsonSchemaProperty ArrayProperty(JsonSchema item) - { - return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }; - } - - public static JsonSchemaProperty BooleanProperty() - { - return new JsonSchemaProperty { Type = JsonObjectType.Boolean }; - } - - public static JsonSchemaProperty DateTimeProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty GuidProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty NumberProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty ObjectProperty(JsonSchema item, string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired }; - } - - public static JsonSchemaProperty StringProperty(string description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs deleted file mode 100644 index 878591922..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public sealed class ContentSchemaBuilder - { - public JsonSchema CreateContentSchema(Schema schema, JsonSchema dataSchema) - { - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(dataSchema, nameof(dataSchema)); - - var schemaName = schema.Properties.Label.WithFallback(schema.Name); - - var contentSchema = new JsonSchema - { - Properties = - { - ["id"] = Builder.GuidProperty($"The id of the {schemaName} content.", true), - ["data"] = Builder.ObjectProperty(dataSchema, $"The data of the {schemaName}.", true), - ["dataDraft"] = Builder.ObjectProperty(dataSchema, $"The draft data of the {schemaName}.", false), - ["version"] = Builder.NumberProperty($"The version of the {schemaName}.", true), - ["created"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been created.", true), - ["createdBy"] = Builder.StringProperty($"The user that has created the {schemaName} content.", true), - ["lastModified"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been modified last.", true), - ["lastModifiedBy"] = Builder.StringProperty($"The user that has updated the {schemaName} content last.", true), - ["status"] = Builder.StringProperty($"The status of the content.", true) - }, - Type = JsonObjectType.Object - }; - - return contentSchema; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs deleted file mode 100644 index ef1243f3e..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public static class JsonSchemaExtensions - { - public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, SchemaResolver schemaResolver, bool withHidden = false) - { - Guard.NotNull(schemaResolver, nameof(schemaResolver)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - var schemaName = schema.Name.ToPascalCase(); - - var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver, withHidden); - var jsonSchema = Builder.Object(); - - foreach (var field in schema.Fields.ForApi(withHidden)) - { - var partitionObject = Builder.Object(); - var partitionSet = partitionResolver(field.Partitioning); - - foreach (var partitionItem in partitionSet) - { - var partitionItemProperty = field.Accept(jsonTypeVisitor); - - if (partitionItemProperty != null) - { - partitionItemProperty.Description = partitionItem.Name; - partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional; - - partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); - } - } - - if (partitionObject.Properties.Count > 0) - { - var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); - - jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference)); - } - } - - return jsonSchema; - } - - public static JsonSchemaProperty CreateProperty(IField field, JsonSchema reference) - { - var jsonProperty = Builder.ObjectProperty(reference); - - if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) - { - jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})"; - } - else - { - jsonProperty.Description = field.Name; - } - - jsonProperty.IsRequired = field.RawProperties.IsRequired; - - return jsonProperty; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs deleted file mode 100644 index f83c3ceb1..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ /dev/null @@ -1,151 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public delegate JsonSchema SchemaResolver(string name, JsonSchema schema); - - public sealed class JsonTypeVisitor : IFieldVisitor - { - private readonly SchemaResolver schemaResolver; - private readonly bool withHiddenFields; - - public JsonTypeVisitor(SchemaResolver schemaResolver, bool withHiddenFields) - { - this.schemaResolver = schemaResolver; - - this.withHiddenFields = withHiddenFields; - } - - public JsonSchemaProperty Visit(IArrayField field) - { - var item = Builder.Object(); - - foreach (var nestedField in field.Fields.ForApi(withHiddenFields)) - { - var childProperty = nestedField.Accept(this); - - if (childProperty != null) - { - childProperty.Description = nestedField.RawProperties.Hints; - childProperty.IsRequired = nestedField.RawProperties.IsRequired; - - item.Properties.Add(nestedField.Name, childProperty); - } - } - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - var item = schemaResolver("AssetItem", Builder.Guid()); - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - return Builder.BooleanProperty(); - } - - public JsonSchemaProperty Visit(IField field) - { - return Builder.DateTimeProperty(); - } - - public JsonSchemaProperty Visit(IField field) - { - var geolocationSchema = Builder.Object(); - - geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty - { - Type = JsonObjectType.Number, - Minimum = -90, - Maximum = 90, - IsRequired = true - }); - - geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty - { - Type = JsonObjectType.Number, - Minimum = -180, - Maximum = 180, - IsRequired = true - }); - - var reference = schemaResolver("GeolocationDto", geolocationSchema); - - return Builder.ObjectProperty(reference); - } - - public JsonSchemaProperty Visit(IField field) - { - return Builder.StringProperty(); - } - - public JsonSchemaProperty Visit(IField field) - { - var property = Builder.NumberProperty(); - - if (field.Properties.MinValue.HasValue) - { - property.Minimum = (decimal)field.Properties.MinValue.Value; - } - - if (field.Properties.MaxValue.HasValue) - { - property.Maximum = (decimal)field.Properties.MaxValue.Value; - } - - return property; - } - - public JsonSchemaProperty Visit(IField field) - { - var item = schemaResolver("ReferenceItem", Builder.Guid()); - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - var property = Builder.StringProperty(); - - property.MinLength = field.Properties.MinLength; - property.MaxLength = field.Properties.MaxLength; - - if (field.Properties.AllowedValues != null) - { - var names = property.EnumerationNames = property.EnumerationNames ?? new Collection(); - - foreach (var value in field.Properties.AllowedValues) - { - names.Add(value); - } - } - - return property; - } - - public JsonSchemaProperty Visit(IField field) - { - var item = schemaResolver("ReferenceItem", Builder.String()); - - return Builder.ArrayProperty(item); - } - - public JsonSchemaProperty Visit(IField field) - { - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs deleted file mode 100644 index e21540f2d..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Runtime.Serialization; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents -{ - public abstract class EnrichedUserEventBase : EnrichedEvent - { - public RefToken Actor { get; set; } - - [IgnoreDataMember] - public IUser User { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs deleted file mode 100644 index b8515b1c5..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class EventEnricher : IEventEnricher - { - private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); - private readonly IMemoryCache userCache; - private readonly IUserResolver userResolver; - - public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) - { - Guard.NotNull(userCache, nameof(userCache)); - Guard.NotNull(userResolver, nameof(userResolver)); - - this.userCache = userCache; - this.userResolver = userResolver; - } - - public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event) - { - enrichedEvent.Timestamp = @event.Headers.Timestamp(); - - if (enrichedEvent is EnrichedUserEventBase userEvent) - { - if (@event.Payload is SquidexEvent squidexEvent) - { - userEvent.Actor = squidexEvent.Actor; - } - - userEvent.User = await FindUserAsync(userEvent.Actor); - } - - enrichedEvent.AppId = @event.Payload.AppId; - } - - private Task FindUserAsync(RefToken actor) - { - var key = $"EventEnrichers_Users_${actor.Identifier}"; - - return userCache.GetOrCreateAsync(key, async x => - { - x.AbsoluteExpirationRelativeToNow = UserCacheDuration; - - IUser user; - try - { - user = await userResolver.FindByIdOrEmailAsync(actor.Identifier); - } - catch - { - user = null; - } - - if (user == null && actor.IsClient) - { - user = new ClientUser(actor); - } - - return user; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs deleted file mode 100644 index b7cd99f60..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public interface IRuleTriggerHandler - { - Type TriggerType { get; } - - Task CreateEnrichedEventAsync(Envelope @event); - - bool Trigger(EnrichedEvent @event, RuleTrigger trigger); - - bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs deleted file mode 100644 index d73750a63..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class Result - { - public Exception Exception { get; private set; } - - public string Dump { get; private set; } - - public RuleResult Status { get; private set; } - - public void Enrich(TimeSpan elapsed) - { - var dumpBuilder = new StringBuilder(); - - if (!string.IsNullOrWhiteSpace(Dump)) - { - dumpBuilder.AppendLine(Dump); - } - - if (Status == RuleResult.Timeout) - { - dumpBuilder.AppendLine(); - dumpBuilder.AppendLine("Action timed out."); - } - - dumpBuilder.AppendLine(); - dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); - dumpBuilder.AppendLine(); - - Dump = dumpBuilder.ToString(); - } - - public static Result Ignored() - { - return Success("Ignored"); - } - - public static Result Complete() - { - return Success("Completed"); - } - - public static Result Create(string dump, RuleResult result) - { - return new Result { Dump = dump, Status = result }; - } - - public static Result Success(string dump) - { - return new Result { Dump = dump, Status = RuleResult.Success }; - } - - public static Result Failed(Exception ex) - { - return Failed(ex, ex?.Message); - } - - public static Result SuccessOrFailed(Exception ex, string dump) - { - if (ex != null) - { - return Failed(ex, dump); - } - else - { - return Success(dump); - } - } - - public static Result Failed(Exception ex, string dump) - { - var result = new Result { Exception = ex, Dump = dump ?? ex.Message }; - - if (ex is OperationCanceledException || ex is TimeoutException) - { - result.Status = RuleResult.Timeout; - } - else - { - result.Status = RuleResult.Failed; - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs deleted file mode 100644 index cbc5d7698..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction - { - private readonly RuleEventFormatter formatter; - - Type IRuleActionHandler.ActionType - { - get { return typeof(TAction); } - } - - Type IRuleActionHandler.DataType - { - get { return typeof(TData); } - } - - protected RuleActionHandler(RuleEventFormatter formatter) - { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; - } - - protected virtual string ToJson(T @event) - { - return formatter.ToPayload(@event); - } - - protected virtual string ToEnvelopeJson(EnrichedEvent @event) - { - return formatter.ToEnvelope(@event); - } - - protected string Format(Uri uri, EnrichedEvent @event) - { - return formatter.Format(uri.ToString(), @event); - } - - protected string Format(string text, EnrichedEvent @event) - { - return formatter.Format(text, @event); - } - - async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) - { - var (description, data) = await CreateJobAsync(@event, (TAction)action); - - return (description, data); - } - - async Task IRuleActionHandler.ExecuteJobAsync(object data, CancellationToken ct) - { - var typedData = (TData)data; - - return await ExecuteJobAsync(typedData, ct); - } - - protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) - { - return Task.FromResult(CreateJob(@event, action)); - } - - protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) - { - throw new NotImplementedException(); - } - - protected abstract Task ExecuteJobAsync(TData job, CancellationToken ct = default); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs deleted file mode 100644 index 1611e76ea..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class RuleActionProperty - { - public RuleActionPropertyEditor Editor { get; set; } - - public string Name { get; set; } - - public string Display { get; set; } - - public string Description { get; set; } - - public bool IsFormattable { get; set; } - - public bool IsRequired { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs deleted file mode 100644 index 2d0477228..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class RuleActionRegistration - { - public Type ActionType { get; } - - internal RuleActionRegistration(Type actionType) - { - Guard.NotNull(actionType, nameof(actionType)); - - ActionType = actionType; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs deleted file mode 100644 index 6aeb22e82..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ /dev/null @@ -1,314 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// =========================================-================================= - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public class RuleEventFormatter - { - private const string Fallback = "null"; - private const string ScriptSuffix = ")"; - private const string ScriptPrefix = "Script("; - private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray(); - private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray(); - private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); - private static readonly Regex ContentDataPlaceholderNew = new Regex(@"^\{CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}\}", RegexOptions.Compiled); - private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>(); - private readonly IJsonSerializer jsonSerializer; - private readonly IRuleUrlGenerator urlGenerator; - private readonly IScriptEngine scriptEngine; - - public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(urlGenerator, nameof(urlGenerator)); - - this.jsonSerializer = jsonSerializer; - this.scriptEngine = scriptEngine; - this.urlGenerator = urlGenerator; - - AddPattern("APP_ID", AppId); - AddPattern("APP_NAME", AppName); - AddPattern("CONTENT_ACTION", ContentAction); - AddPattern("CONTENT_STATUS", ContentStatus); - AddPattern("CONTENT_URL", ContentUrl); - AddPattern("SCHEMA_ID", SchemaId); - AddPattern("SCHEMA_NAME", SchemaName); - AddPattern("TIMESTAMP_DATETIME", TimestampTime); - AddPattern("TIMESTAMP_DATE", TimestampDate); - AddPattern("USER_ID", UserId); - AddPattern("USER_NAME", UserName); - AddPattern("USER_EMAIL", UserEmail); - } - - private void AddPattern(string placeholder, Func generator) - { - patterns.Add((placeholder.ToCharArray(), generator)); - } - - public virtual string ToPayload(T @event) - { - return jsonSerializer.Serialize(@event); - } - - public virtual string ToEnvelope(EnrichedEvent @event) - { - return jsonSerializer.Serialize(new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }); - } - - public string Format(string text, EnrichedEvent @event) - { - if (string.IsNullOrWhiteSpace(text)) - { - return text; - } - - var trimmed = text.Trim(); - - if (trimmed.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && trimmed.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase)) - { - var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length); - - var customFunctions = new Dictionary> - { - ["contentUrl"] = () => ContentUrl(@event), - ["contentAction"] = () => ContentAction(@event) - }; - - return scriptEngine.Interpolate("event", @event, script, customFunctions); - } - - var current = text.AsSpan(); - - var sb = new StringBuilder(); - - var cp2 = new ReadOnlySpan(ContentPlaceholderStartNew); - var cp1 = new ReadOnlySpan(ContentPlaceholderStartOld); - - for (var i = 0; i < current.Length; i++) - { - var c = current[i]; - - if (c == '$') - { - sb.Append(current.Slice(0, i).ToString()); - - current = current.Slice(i); - - var test = current.Slice(1); - var tested = false; - - for (var j = 0; j < patterns.Count; j++) - { - var (pattern, replacer) = patterns[j]; - - if (test.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) - { - sb.Append(replacer(@event)); - - current = current.Slice(pattern.Length + 1); - i = 0; - - tested = true; - break; - } - } - - if (!tested && (test.StartsWith(cp1, StringComparison.OrdinalIgnoreCase) || test.StartsWith(cp2, StringComparison.OrdinalIgnoreCase))) - { - var currentString = test.ToString(); - - var match = ContentDataPlaceholderOld.Match(currentString); - - if (!match.Success) - { - match = ContentDataPlaceholderNew.Match(currentString); - } - - if (match.Success) - { - if (@event is EnrichedContentEvent contentEvent) - { - sb.Append(CalculateData(contentEvent.Data, match)); - } - else - { - sb.Append(Fallback); - } - - current = current.Slice(match.Length + 1); - i = 0; - } - } - } - } - - sb.Append(current.ToString()); - - return sb.ToString(); - } - - private static string TimestampDate(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); - } - - private static string TimestampTime(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); - } - - private static string AppId(EnrichedEvent @event) - { - return @event.AppId.Id.ToString(); - } - - private static string AppName(EnrichedEvent @event) - { - return @event.AppId.Name; - } - - private static string SchemaId(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Id.ToString(); - } - - return Fallback; - } - - private static string SchemaName(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Name; - } - - return Fallback; - } - - private static string ContentAction(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return contentEvent.Type.ToString(); - } - - return Fallback; - } - - private static string ContentStatus(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return contentEvent.Status.ToString(); - } - - return Fallback; - } - - private string ContentUrl(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return urlGenerator.GenerateContentUIUrl(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); - } - - return Fallback; - } - - private static string UserName(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.DisplayName() ?? Fallback; - } - - return Fallback; - } - - private static string UserId(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Id ?? Fallback; - } - - return Fallback; - } - - private static string UserEmail(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Email ?? Fallback; - } - - return Fallback; - } - - private static string CalculateData(NamedContentData data, Match match) - { - var captures = match.Groups[2].Captures; - - var path = new string[captures.Count]; - - for (var i = 0; i < path.Length; i++) - { - path[i] = captures[i].Value; - } - - if (!data.TryGetValue(path[0], out var field)) - { - return Fallback; - } - - if (!field.TryGetValue(path[1], out var value)) - { - return Fallback; - } - - for (var j = 2; j < path.Length; j++) - { - if (value is JsonObject obj && obj.TryGetValue(path[j], out value)) - { - continue; - } - - if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count) - { - value = array[idx]; - } - else - { - return Fallback; - } - } - - if (value == null || value.Type == JsonValueType.Null) - { - return Fallback; - } - - return value.ToString() ?? Fallback; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs deleted file mode 100644 index 8666a5206..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable RECS0033 // Convert 'if' to '||' expression - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public sealed class RuleRegistry : ITypeProvider - { - private const string ActionSuffix = "Action"; - private const string ActionSuffixV2 = "ActionV2"; - private readonly Dictionary actionTypes = new Dictionary(); - - public IReadOnlyDictionary Actions - { - get { return actionTypes; } - } - - public RuleRegistry(IEnumerable registrations = null) - { - if (registrations != null) - { - foreach (var registration in registrations) - { - Add(registration.ActionType); - } - } - } - - public void Add() where T : RuleAction - { - Add(typeof(T)); - } - - private void Add(Type actionType) - { - var metadata = actionType.GetCustomAttribute(); - - if (metadata == null) - { - return; - } - - var name = GetActionName(actionType); - - var definition = - new RuleActionDefinition - { - Type = actionType, - Title = metadata.Title, - Display = metadata.Display, - Description = metadata.Description, - IconColor = metadata.IconColor, - IconImage = metadata.IconImage, - ReadMore = metadata.ReadMore - }; - - foreach (var property in actionType.GetProperties()) - { - if (property.CanRead && property.CanWrite) - { - var actionProperty = new RuleActionProperty { Name = property.Name.ToCamelCase(), Display = property.Name }; - - var display = property.GetCustomAttribute(); - - if (!string.IsNullOrWhiteSpace(display?.Name)) - { - actionProperty.Display = display.Name; - } - - if (!string.IsNullOrWhiteSpace(display?.Description)) - { - actionProperty.Description = display.Description; - } - - var type = property.PropertyType; - - if ((GetDataAttribute(property) != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?)) - { - actionProperty.IsRequired = true; - } - - if (property.GetCustomAttribute() != null) - { - actionProperty.IsFormattable = true; - } - - var dataType = GetDataAttribute(property)?.DataType; - - if (type == typeof(bool) || type == typeof(bool?)) - { - actionProperty.Editor = RuleActionPropertyEditor.Checkbox; - } - else if (type == typeof(int) || type == typeof(int?)) - { - actionProperty.Editor = RuleActionPropertyEditor.Number; - } - else if (dataType == DataType.Url) - { - actionProperty.Editor = RuleActionPropertyEditor.Url; - } - else if (dataType == DataType.Password) - { - actionProperty.Editor = RuleActionPropertyEditor.Password; - } - else if (dataType == DataType.EmailAddress) - { - actionProperty.Editor = RuleActionPropertyEditor.Email; - } - else if (dataType == DataType.MultilineText) - { - actionProperty.Editor = RuleActionPropertyEditor.TextArea; - } - else - { - actionProperty.Editor = RuleActionPropertyEditor.Text; - } - - definition.Properties.Add(actionProperty); - } - } - - actionTypes[name] = definition; - } - - private static T GetDataAttribute(PropertyInfo property) where T : ValidationAttribute - { - var result = property.GetCustomAttribute(); - - result?.IsValid(null); - - return result; - } - - private static bool IsNullable(Type type) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - private static string GetActionName(Type type) - { - return type.TypeName(false, ActionSuffix, ActionSuffixV2); - } - - public void Map(TypeNameRegistry typeNameRegistry) - { - foreach (var actionType in actionTypes.Values) - { - typeNameRegistry.Map(actionType.Type, actionType.Type.Name); - } - - var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract); - - var addedTypes = new HashSet(); - - foreach (var type in eventTypes) - { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.Name); - } - } - - var triggerTypes = typeof(RuleTrigger).Assembly.GetTypes().Where(x => typeof(RuleTrigger).IsAssignableFrom(x) && !x.IsAbstract); - - foreach (var type in triggerTypes) - { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.Name); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs deleted file mode 100644 index 21319f7de..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ /dev/null @@ -1,202 +0,0 @@ -// ========================================================================== -// 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 System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using NodaTime; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public class RuleService - { - private readonly Dictionary ruleActionHandlers; - private readonly Dictionary ruleTriggerHandlers; - private readonly TypeNameRegistry typeNameRegistry; - private readonly RuleOptions ruleOptions; - private readonly IEventEnricher eventEnricher; - private readonly IJsonSerializer jsonSerializer; - private readonly IClock clock; - private readonly ISemanticLog log; - - public RuleService( - IOptions ruleOptions, - IEnumerable ruleTriggerHandlers, - IEnumerable ruleActionHandlers, - IEventEnricher eventEnricher, - IJsonSerializer jsonSerializer, - IClock clock, - ISemanticLog log, - TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - Guard.NotNull(ruleOptions, nameof(ruleOptions)); - Guard.NotNull(ruleTriggerHandlers, nameof(ruleTriggerHandlers)); - Guard.NotNull(ruleActionHandlers, nameof(ruleActionHandlers)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - Guard.NotNull(eventEnricher, nameof(eventEnricher)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.typeNameRegistry = typeNameRegistry; - - this.ruleOptions = ruleOptions.Value; - this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); - this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); - this.eventEnricher = eventEnricher; - - this.jsonSerializer = jsonSerializer; - - this.clock = clock; - - this.log = log; - } - - public virtual async Task CreateJobAsync(Rule rule, Guid ruleId, Envelope @event) - { - Guard.NotNull(rule, nameof(rule)); - Guard.NotNull(@event, nameof(@event)); - - try - { - if (!rule.IsEnabled) - { - return null; - } - - if (!(@event.Payload is AppEvent)) - { - return null; - } - - var typed = @event.To(); - - var actionType = rule.Action.GetType(); - - if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) - { - return null; - } - - if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) - { - return null; - } - - var now = clock.GetCurrentInstant(); - - var eventTime = - @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? - @event.Headers.Timestamp() : - now; - - var expires = eventTime.Plus(Constants.ExpirationTime); - - if (eventTime.Plus(Constants.StaleTime) < now) - { - return null; - } - - if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) - { - return null; - } - - var appEventEnvelope = @event.To(); - - var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); - - if (enrichedEvent == null) - { - return null; - } - - await eventEnricher.EnrichAsync(enrichedEvent, typed); - - if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) - { - return null; - } - - var actionName = typeNameRegistry.GetName(actionType); - var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); - - var json = jsonSerializer.Serialize(actionData.Data); - - var job = new RuleJob - { - Id = Guid.NewGuid(), - ActionData = json, - ActionName = actionName, - AppId = enrichedEvent.AppId.Id, - Created = now, - Description = actionData.Description, - EventName = enrichedEvent.Name, - ExecutionPartition = enrichedEvent.Partition, - Expires = expires, - RuleId = ruleId - }; - - return job; - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "createRuleJob") - .WriteProperty("status", "Failed")); - - return null; - } - } - - public virtual async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) - { - var actionWatch = ValueStopwatch.StartNew(); - - Result result; - - try - { - var actionType = typeNameRegistry.GetType(actionName); - var actionHandler = ruleActionHandlers[actionType]; - - var deserialized = jsonSerializer.Deserialize(job, actionHandler.DataType); - - using (var cts = new CancellationTokenSource(GetTimeoutInMs())) - { - result = await actionHandler.ExecuteJobAsync(deserialized, cts.Token).WithCancellation(cts.Token); - } - } - catch (Exception ex) - { - result = Result.Failed(ex); - } - - var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); - - result.Enrich(elapsed); - - return (result, elapsed); - } - - private int GetTimeoutInMs() - { - return ruleOptions.ExecutionTimeoutInSeconds * 1000; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs deleted file mode 100644 index c369497ac..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -#pragma warning disable IDE0019 // Use pattern matching - -namespace Squidex.Domain.Apps.Core.HandleRules -{ - public abstract class RuleTriggerHandler : IRuleTriggerHandler - where TTrigger : RuleTrigger - where TEvent : AppEvent - where TEnrichedEvent : EnrichedEvent - { - public Type TriggerType - { - get { return typeof(TTrigger); } - } - - async Task IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) - { - return await CreateEnrichedEventAsync(@event.To()); - } - - bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) - { - if (@event is TEnrichedEvent typed) - { - return Trigger(typed, (TTrigger)trigger); - } - - return false; - } - - bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) - { - if (@event is TEvent typed) - { - return Trigger(typed, (TTrigger)trigger, ruleId); - } - - return false; - } - - protected abstract Task CreateEnrichedEventAsync(Envelope @event); - - protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); - - protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) - { - return true; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs deleted file mode 100644 index e75e1f0b8..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ /dev/null @@ -1,130 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Jint; -using Jint.Native; -using Jint.Native.Object; -using Jint.Runtime.Descriptors; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -#pragma warning disable RECS0133 // Parameter name differs in base declaration - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentDataObject : ObjectInstance - { - private readonly NamedContentData contentData; - private HashSet fieldsToDelete; - private Dictionary fieldProperties; - private bool isChanged; - - public ContentDataObject(Engine engine, NamedContentData contentData) - : base(engine) - { - Extensible = true; - - this.contentData = contentData; - } - - public void MarkChanged() - { - isChanged = true; - } - - public bool TryUpdate(out NamedContentData result) - { - result = contentData; - - if (isChanged) - { - if (fieldsToDelete != null) - { - foreach (var field in fieldsToDelete) - { - contentData.Remove(field); - } - } - - if (fieldProperties != null) - { - foreach (var kvp in fieldProperties) - { - var value = (ContentDataProperty)kvp.Value; - - if (value.ContentField.TryUpdate(out var fieldData)) - { - contentData[kvp.Key] = fieldData; - } - } - } - } - - return isChanged; - } - - public override void RemoveOwnProperty(string propertyName) - { - if (fieldsToDelete == null) - { - fieldsToDelete = new HashSet(); - } - - fieldsToDelete.Add(propertyName); - fieldProperties?.Remove(propertyName); - - MarkChanged(); - } - - public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) - { - EnsurePropertiesInitialized(); - - if (!fieldProperties.ContainsKey(propertyName)) - { - fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; - } - - return true; - } - - public override void Put(string propertyName, JsValue value, bool throwOnError) - { - EnsurePropertiesInitialized(); - - fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c)).Value = value; - } - - public override PropertyDescriptor GetOwnProperty(string propertyName) - { - EnsurePropertiesInitialized(); - - return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false))); - } - - public override IEnumerable> GetOwnProperties() - { - EnsurePropertiesInitialized(); - - return fieldProperties; - } - - private void EnsurePropertiesInitialized() - { - if (fieldProperties == null) - { - fieldProperties = new Dictionary(contentData.Count); - - foreach (var kvp in contentData) - { - fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs deleted file mode 100644 index b9a51cfbf..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Jint.Native; -using Jint.Runtime; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentDataProperty : CustomProperty - { - private readonly ContentDataObject contentData; - private ContentFieldObject contentField; - private JsValue value; - - protected override JsValue CustomValue - { - get - { - return value; - } - set - { - if (!Equals(this.value, value)) - { - if (value == null || !value.IsObject()) - { - throw new JavaScriptException("You can only assign objects to content data."); - } - - var obj = value.AsObject(); - - contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); - - foreach (var kvp in obj.GetOwnProperties()) - { - contentField.Put(kvp.Key, kvp.Value.Value, true); - } - - this.value = contentField; - } - } - } - - public ContentFieldObject ContentField - { - get { return contentField; } - } - - public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null) - { - this.contentData = contentData; - this.contentField = contentField; - - if (contentField != null) - { - value = contentField; - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs deleted file mode 100644 index d6ef06266..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ /dev/null @@ -1,135 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Jint.Native.Object; -using Jint.Runtime.Descriptors; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -#pragma warning disable RECS0133 // Parameter name differs in base declaration - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentFieldObject : ObjectInstance - { - private readonly ContentDataObject contentData; - private readonly ContentFieldData fieldData; - private HashSet valuesToDelete; - private Dictionary valueProperties; - private bool isChanged; - - public ContentFieldData FieldData - { - get { return fieldData; } - } - - public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew) - : base(contentData.Engine) - { - Extensible = true; - - this.contentData = contentData; - this.fieldData = fieldData; - - if (isNew) - { - MarkChanged(); - } - } - - public void MarkChanged() - { - isChanged = true; - - contentData.MarkChanged(); - } - - public bool TryUpdate(out ContentFieldData result) - { - result = fieldData; - - if (isChanged) - { - if (valuesToDelete != null) - { - foreach (var field in valuesToDelete) - { - fieldData.Remove(field); - } - } - - if (valueProperties != null) - { - foreach (var kvp in valueProperties) - { - var value = (ContentFieldProperty)kvp.Value; - - if (value.IsChanged) - { - fieldData[kvp.Key] = value.ContentValue; - } - } - } - } - - return isChanged; - } - - public override void RemoveOwnProperty(string propertyName) - { - if (valuesToDelete == null) - { - valuesToDelete = new HashSet(); - } - - valuesToDelete.Add(propertyName); - valueProperties?.Remove(propertyName); - - MarkChanged(); - } - - public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) - { - EnsurePropertiesInitialized(); - - if (!valueProperties.ContainsKey(propertyName)) - { - valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; - } - - return true; - } - - public override PropertyDescriptor GetOwnProperty(string propertyName) - { - EnsurePropertiesInitialized(); - - return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; - } - - public override IEnumerable> GetOwnProperties() - { - EnsurePropertiesInitialized(); - - return valueProperties; - } - - private void EnsurePropertiesInitialized() - { - if (valueProperties == null) - { - valueProperties = new Dictionary(FieldData.Count); - - foreach (var kvp in FieldData) - { - valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs deleted file mode 100644 index ed5aa34d2..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Jint.Native; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public sealed class ContentFieldProperty : CustomProperty - { - private readonly ContentFieldObject contentField; - private IJsonValue contentValue; - private JsValue value; - private bool isChanged; - - protected override JsValue CustomValue - { - get - { - return value ?? (value = JsonMapper.Map(contentValue, contentField.Engine)); - } - set - { - if (!Equals(this.value, value)) - { - this.value = value; - - contentValue = null; - contentField.MarkChanged(); - - isChanged = true; - } - } - } - - public IJsonValue ContentValue - { - get { return contentValue ?? (contentValue = JsonMapper.Map(value)); } - } - - public bool IsChanged - { - get { return isChanged; } - } - - public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null) - { - this.contentField = contentField; - this.contentValue = contentValue; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs deleted file mode 100644 index 4720ddfce..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Jint; -using Jint.Native; -using Jint.Native.Object; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper -{ - public static class JsonMapper - { - public static JsValue Map(IJsonValue value, Engine engine) - { - if (value == null) - { - return JsValue.Null; - } - - switch (value) - { - case JsonNull _: - return JsValue.Null; - case JsonScalar s: - return new JsString(s.Value); - case JsonScalar b: - return new JsBoolean(b.Value); - case JsonScalar b: - return new JsNumber(b.Value); - case JsonObject obj: - return FromObject(obj, engine); - case JsonArray arr: - return FromArray(arr, engine); - } - - throw new ArgumentException("Invalid json type.", nameof(value)); - } - - private static JsValue FromArray(JsonArray arr, Engine engine) - { - var target = new JsValue[arr.Count]; - - for (var i = 0; i < arr.Count; i++) - { - target[i] = Map(arr[i], engine); - } - - return engine.Array.Construct(target); - } - - private static JsValue FromObject(JsonObject obj, Engine engine) - { - var target = new ObjectInstance(engine); - - foreach (var property in obj) - { - target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true); - } - - return target; - } - - public static IJsonValue Map(JsValue value) - { - if (value == null || value.IsNull() || value.IsUndefined()) - { - return JsonValue.Null; - } - - if (value.IsString()) - { - return JsonValue.Create(value.AsString()); - } - - if (value.IsBoolean()) - { - return JsonValue.Create(value.AsBoolean()); - } - - if (value.IsNumber()) - { - return JsonValue.Create(value.AsNumber()); - } - - if (value.IsDate()) - { - return JsonValue.Create(value.AsDate().ToString()); - } - - if (value.IsRegExp()) - { - return JsonValue.Create(value.AsRegExp().Value?.ToString()); - } - - if (value.IsArray()) - { - var arr = value.AsArray(); - - var result = JsonValue.Array(); - - for (var i = 0; i < arr.GetLength(); i++) - { - result.Add(Map(arr.Get(i.ToString()))); - } - - return result; - } - - if (value.IsObject()) - { - var obj = value.AsObject(); - - var result = JsonValue.Object(); - - foreach (var kvp in obj.GetOwnProperties()) - { - result[kvp.Key] = Map(kvp.Value.Value); - } - - return result; - } - - throw new ArgumentException("Invalid json type.", nameof(value)); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs deleted file mode 100644 index 4cd2a9007..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Jint; -using Jint.Native; -using Jint.Runtime.Interop; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed class DefaultConverter : IObjectConverter - { - public static readonly DefaultConverter Instance = new DefaultConverter(); - - private DefaultConverter() - { - } - - public bool TryConvert(Engine engine, object value, out JsValue result) - { - result = null; - - if (value is Enum) - { - result = value.ToString(); - return true; - } - - switch (value) - { - case IUser user: - result = JintUser.Create(engine, user); - return true; - case ClaimsPrincipal principal: - result = JintUser.Create(engine, principal); - return true; - case Instant instant: - result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); - return true; - case Status status: - result = status.ToString(); - return true; - case NamedContentData content: - result = new ContentDataObject(engine, content); - return true; - } - - return false; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs deleted file mode 100644 index 55742c99d..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public interface IScriptEngine - { - void Execute(ScriptContext context, string script); - - NamedContentData ExecuteAndTransform(ScriptContext context, string script); - - NamedContentData Transform(ScriptContext context, string script); - - bool Evaluate(string name, object context, string script); - - string Interpolate(string name, object context, string script, Dictionary> customFormatters = null); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs deleted file mode 100644 index 4f04a6343..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ /dev/null @@ -1,312 +0,0 @@ -// ========================================================================== -// 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.Globalization; -using Esprima; -using Jint; -using Jint.Native; -using Jint.Native.Date; -using Jint.Native.Object; -using Jint.Runtime; -using Jint.Runtime.Interop; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed class JintScriptEngine : IScriptEngine - { - public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); - - public void Execute(ScriptContext context, string script) - { - Guard.NotNull(context, nameof(context)); - - if (!string.IsNullOrWhiteSpace(script)) - { - var engine = CreateScriptEngine(context); - - EnableDisallow(engine); - EnableReject(engine); - - Execute(engine, script); - } - } - - public NamedContentData ExecuteAndTransform(ScriptContext context, string script) - { - Guard.NotNull(context, nameof(context)); - - var result = context.Data; - - if (!string.IsNullOrWhiteSpace(script)) - { - var engine = CreateScriptEngine(context); - - EnableDisallow(engine); - EnableReject(engine); - - engine.SetValue("operation", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); - - engine.SetValue("replace", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); - - Execute(engine, script); - } - - return result; - } - - public NamedContentData Transform(ScriptContext context, string script) - { - Guard.NotNull(context, nameof(context)); - - var result = context.Data; - - if (!string.IsNullOrWhiteSpace(script)) - { - try - { - var engine = CreateScriptEngine(context); - - engine.SetValue("replace", new Action(() => - { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) - { - data.TryUpdate(out result); - } - })); - - engine.Execute(script); - } - catch (Exception) - { - result = context.Data; - } - } - - return result; - } - - private static void Execute(Engine engine, string script) - { - try - { - engine.Execute(script); - } - catch (ArgumentException ex) - { - throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); - } - catch (JavaScriptException ex) - { - throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); - } - catch (ParserException ex) - { - throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); - } - } - - private Engine CreateScriptEngine(ScriptContext context) - { - var engine = CreateScriptEngine(); - - var contextInstance = new ObjectInstance(engine); - - if (context.Data != null) - { - contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); - } - - if (context.DataOld != null) - { - contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); - } - - if (context.User != null) - { - contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); - } - - if (!string.IsNullOrWhiteSpace(context.Operation)) - { - contextInstance.FastAddProperty("operation", context.Operation, false, false, false); - } - - contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); - - if (context.StatusOld != default) - { - contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); - } - - engine.SetValue("ctx", contextInstance); - engine.SetValue("context", contextInstance); - - return engine; - } - - private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary> customFormatters = null) - { - var engine = new Engine(options => - { - if (resolver != null) - { - options.SetReferencesResolver(resolver); - } - - options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); - }); - - if (customFormatters != null) - { - foreach (var kvp in customFormatters) - { - engine.SetValue(kvp.Key, Safe(kvp.Value)); - } - } - - engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); - engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); - engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); - - return engine; - } - - private static Func Safe(Func func) - { - return () => - { - try - { - return func(); - } - catch - { - return "null"; - } - }; - } - - private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) - { - try - { - var stringInput = TypeConverter.ToString(arguments.At(0)); - var single = false; - - if (arguments.Length > 1) - { - single = TypeConverter.ToBoolean(arguments.At(1)); - } - - return stringInput.Slugify(null, single); - } - catch - { - return JsValue.Undefined; - } - } - - private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) - { - try - { - var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); - var dateFormat = TypeConverter.ToString(arguments.At(1)); - - return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); - } - catch - { - return JsValue.Undefined; - } - } - - private static void EnableDisallow(Engine engine) - { - engine.SetValue("disallow", new Action(message => - { - var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; - - throw new DomainForbiddenException(exMessage); - })); - } - - private static void EnableReject(Engine engine) - { - engine.SetValue("reject", new Action(message => - { - var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; - - throw new ValidationException("Script rejected the operation.", errors); - })); - } - - public bool Evaluate(string name, object context, string script) - { - try - { - var result = - CreateScriptEngine(NullPropagation.Instance) - .SetValue(name, context) - .Execute(script) - .GetCompletionValue() - .ToObject(); - - return (bool)result; - } - catch - { - return false; - } - } - - public string Interpolate(string name, object context, string script, Dictionary> customFormatters = null) - { - try - { - var result = - CreateScriptEngine(NullPropagation.Instance, customFormatters) - .SetValue(name, context) - .Execute(script) - .GetCompletionValue() - .ToObject(); - - var converted = result.ToString(); - - return converted == "undefined" ? "null" : converted; - } - catch (Exception ex) - { - return ex.Message; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs deleted file mode 100644 index d32a234bd..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Jint; -using Jint.Runtime.Interop; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public static class JintUser - { - private static readonly char[] ClaimSeparators = { '/', '.', ':' }; - - public static ObjectWrapper Create(Engine engine, IUser user) - { - var clientId = user.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; - - var isClient = !string.IsNullOrWhiteSpace(clientId); - - return CreateUser(engine, user.Id, isClient, user.Email, user.DisplayName(), user.Claims); - } - - public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) - { - var id = principal.OpenIdSubject(); - - var isClient = string.IsNullOrWhiteSpace(id); - - if (isClient) - { - id = principal.OpenIdClientId(); - } - - var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value; - - return CreateUser(engine, id, isClient, principal.OpenIdEmail(), name, principal.Claims); - } - - private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable allClaims) - { - var claims = - allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) - .ToDictionary( - x => x.Key, - x => x.Select(y => y.Value).ToArray()); - - return new ObjectWrapper(engine, new { id, isClient, email, name, claims }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs deleted file mode 100644 index 40d4212ae..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed class ScriptContext - { - public ClaimsPrincipal User { get; set; } - - public Guid ContentId { get; set; } - - public NamedContentData Data { get; set; } - - public NamedContentData DataOld { get; set; } - - public Status Status { get; set; } - - public Status StatusOld { get; set; } - - public string Operation { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj deleted file mode 100644 index 07d2f40f4..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - netstandard2.0 - Squidex.Domain.Apps.Core - 7.3 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs deleted file mode 100644 index ad819ba57..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.Tags -{ - public interface ITagService - { - Task> GetTagIdsAsync(Guid appId, string group, HashSet names); - - Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids); - - Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids); - - Task GetTagsAsync(Guid appId, string group); - - Task GetExportableTagsAsync(Guid appId, string group); - - Task RebuildTagsAsync(Guid appId, string group, TagsExport tags); - - Task ClearAsync(Guid appId, string group); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs deleted file mode 100644 index 7ecac0baf..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.Tags -{ - public static class TagNormalizer - { - public static async Task NormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, NamedContentData newData, NamedContentData oldData) - { - Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(newData, nameof(newData)); - - var newValues = new HashSet(); - var newArrays = new List(); - - var oldValues = new HashSet(); - var oldArrays = new List(); - - GetValues(schema, newValues, newArrays, newData); - - if (oldData != null) - { - GetValues(schema, oldValues, oldArrays, oldData); - } - - if (newValues.Count > 0) - { - var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), newValues, oldValues); - - foreach (var array in newArrays) - { - for (var i = 0; i < array.Count; i++) - { - if (normalized.TryGetValue(array[i].ToString(), out var result)) - { - array[i] = JsonValue.Create(result); - } - } - } - } - } - - public static async Task DenormalizeAsync(this ITagService tagService, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas) - { - Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(schema, nameof(schema)); - - var tagsValues = new HashSet(); - var tagsArrays = new List(); - - GetValues(schema, tagsValues, tagsArrays, datas); - - if (tagsValues.Count > 0) - { - var denormalized = await tagService.DenormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), tagsValues); - - foreach (var array in tagsArrays) - { - for (var i = 0; i < array.Count; i++) - { - if (denormalized.TryGetValue(array[i].ToString(), out var result)) - { - array[i] = JsonValue.Create(result); - } - } - } - } - } - - private static void GetValues(Schema schema, HashSet values, List arrays, params NamedContentData[] datas) - { - foreach (var field in schema.Fields) - { - if (field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema) - { - foreach (var data in datas) - { - if (data.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partition in fieldData) - { - ExtractTags(partition.Value, values, arrays); - } - } - } - } - else if (field is IArrayField arrayField) - { - foreach (var nestedField in arrayField.Fields) - { - if (nestedField is IField nestedTags && nestedTags.Properties.Normalization == TagsFieldNormalization.Schema) - { - foreach (var data in datas) - { - if (data.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partition in fieldData) - { - if (partition.Value is JsonArray array) - { - foreach (var value in array) - { - if (value is JsonObject nestedObject) - { - if (nestedObject.TryGetValue(nestedField.Name, out var nestedValue)) - { - ExtractTags(nestedValue, values, arrays); - } - } - } - } - } - } - } - } - } - } - } - } - - private static void ExtractTags(IJsonValue value, ISet values, ICollection arrays) - { - if (value is JsonArray array) - { - foreach (var item in array) - { - if (item.Type == JsonValueType.String) - { - values.Add(item.ToString()); - } - } - - arrays.Add(array); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs deleted file mode 100644 index ca3c908a6..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; - -#pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class ContentValidator - { - private readonly Schema schema; - private readonly PartitionResolver partitionResolver; - private readonly ValidationContext context; - private readonly ConcurrentBag errors = new ConcurrentBag(); - - public IReadOnlyCollection Errors - { - get { return errors; } - } - - public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) - { - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(context, nameof(context)); - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - this.schema = schema; - this.context = context; - this.partitionResolver = partitionResolver; - } - - private void AddError(IEnumerable path, string message) - { - var pathString = path.ToPathString(); - - errors.Add(new ValidationError(message, pathString)); - } - - public Task ValidatePartialAsync(NamedContentData data) - { - Guard.NotNull(data, nameof(data)); - - var validator = CreateSchemaValidator(true); - - return validator.ValidateAsync(data, context, AddError); - } - - public Task ValidateAsync(NamedContentData data) - { - Guard.NotNull(data, nameof(data)); - - var validator = CreateSchemaValidator(false); - - return validator.ValidateAsync(data, context, AddError); - } - - private IValidator CreateSchemaValidator(bool isPartial) - { - var fieldsValidators = new Dictionary(schema.Fields.Count); - - foreach (var field in schema.Fields) - { - fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); - } - - return new ObjectValidator(fieldsValidators, isPartial, "field"); - } - - private IValidator CreateFieldValidator(IRootField field, bool isPartial) - { - var partitioning = partitionResolver(field.Partitioning); - - var fieldValidator = field.CreateValidator(); - var fieldsValidators = new Dictionary(); - - foreach (var partition in partitioning) - { - fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator); - } - - return new AggregateValidator( - field.CreateBagValidator() - .Union(Enumerable.Repeat( - new ObjectValidator(fieldsValidators, isPartial, TypeName(field)), 1))); - } - - private static string TypeName(IRootField field) - { - var isLanguage = field.Partitioning.Equals(Partitioning.Language); - - return isLanguage ? "language" : "invariant value"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs deleted file mode 100644 index 79747c6b8..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class FieldBagValidatorsFactory : IFieldVisitor> - { - private static readonly FieldBagValidatorsFactory Instance = new FieldBagValidatorsFactory(); - - private FieldBagValidatorsFactory() - { - } - - public static IEnumerable CreateValidators(IField field) - { - Guard.NotNull(field, nameof(field)); - - return field.Accept(Instance); - } - - public IEnumerable Visit(IArrayField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield break; - } - - public IEnumerable Visit(IField field) - { - yield return NoValueValidator.Instance; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs deleted file mode 100644 index 95e179558..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class FieldValueValidatorsFactory : IFieldVisitor> - { - private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory(); - - private FieldValueValidatorsFactory() - { - } - - public static IEnumerable CreateValidators(IField field) - { - Guard.NotNull(field, nameof(field)); - - return field.Accept(Instance); - } - - public IEnumerable Visit(IArrayField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - var nestedSchema = new Dictionary(field.Fields.Count); - - foreach (var nestedField in field.Fields) - { - nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator()); - } - - yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - if (!field.Properties.AllowDuplicates) - { - yield return new UniqueValuesValidator(); - } - - yield return new AssetsValidator(field.Properties); - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - - if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) - { - yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredValidator(); - } - - if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) - { - yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); - } - - if (field.Properties.AllowedValues != null) - { - yield return new AllowedValuesValidator(field.Properties.AllowedValues); - } - - if (field.Properties.IsUnique) - { - yield return new UniqueValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - if (!field.Properties.AllowDuplicates) - { - yield return new UniqueValuesValidator(); - } - - yield return new ReferencesValidator(field.Properties.SchemaIds); - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired) - { - yield return new RequiredStringValidator(true); - } - - if (field.Properties.MinLength.HasValue || field.Properties.MaxLength.HasValue) - { - yield return new StringLengthValidator(field.Properties.MinLength, field.Properties.MaxLength); - } - - if (!string.IsNullOrWhiteSpace(field.Properties.Pattern)) - { - yield return new PatternValidator(field.Properties.Pattern, field.Properties.PatternMessage); - } - - if (field.Properties.AllowedValues != null) - { - yield return new AllowedValuesValidator(field.Properties.AllowedValues); - } - - if (field.Properties.IsUnique) - { - yield return new UniqueValidator(); - } - } - - public IEnumerable Visit(IField field) - { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) - { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); - } - - if (field.Properties.AllowedValues != null) - { - yield return new CollectionItemValidator(new AllowedValuesValidator(field.Properties.AllowedValues)); - } - - yield return new CollectionItemValidator(new RequiredStringValidator(true)); - } - - public IEnumerable Visit(IField field) - { - if (field is INestedField) - { - yield return NoValueValidator.Instance; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs deleted file mode 100644 index 484cb8e38..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ /dev/null @@ -1,231 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime.Text; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public sealed class JsonValueConverter : IFieldVisitor - { - private readonly IJsonValue value; - - private JsonValueConverter(IJsonValue value) - { - this.value = value; - } - - public static object ConvertValue(IField field, IJsonValue json) - { - return field.Accept(new JsonValueConverter(json)); - } - - public object Visit(IArrayField field) - { - return ConvertToObjectList(); - } - - public object Visit(IField field) - { - return ConvertToGuidList(); - } - - public object Visit(IField field) - { - return ConvertToGuidList(); - } - - public object Visit(IField field) - { - return ConvertToStringList(); - } - - public object Visit(IField field) - { - if (value is JsonScalar b) - { - return b.Value; - } - - throw new InvalidCastException("Invalid json type, expected boolean."); - } - - public object Visit(IField field) - { - if (value is JsonScalar b) - { - return b.Value; - } - - throw new InvalidCastException("Invalid json type, expected number."); - } - - public object Visit(IField field) - { - if (value is JsonScalar b) - { - return b.Value; - } - - throw new InvalidCastException("Invalid json type, expected string."); - } - - public object Visit(IField field) - { - return value; - } - - public object Visit(IField field) - { - if (value.Type == JsonValueType.String) - { - var parseResult = InstantPattern.General.Parse(value.ToString()); - - if (!parseResult.Success) - { - throw parseResult.Exception; - } - - return parseResult.Value; - } - - throw new InvalidCastException("Invalid json type, expected string."); - } - - public object Visit(IField field) - { - if (value is JsonObject geolocation) - { - foreach (var propertyName in geolocation.Keys) - { - if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) && - !string.Equals(propertyName, "longitude", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidCastException("Geolocation can only have latitude and longitude property."); - } - } - - if (geolocation.TryGetValue("latitude", out var latValue) && latValue is JsonScalar latNumber) - { - var lat = latNumber.Value; - - if (!lat.IsBetween(-90, 90)) - { - throw new InvalidCastException("Latitude must be between -90 and 90."); - } - } - else - { - throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); - } - - if (geolocation.TryGetValue("longitude", out var lonValue) && lonValue is JsonScalar lonNumber) - { - var lon = lonNumber.Value; - - if (!lon.IsBetween(-180, 180)) - { - throw new InvalidCastException("Longitude must be between -180 and 180."); - } - } - else - { - throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); - } - - return value; - } - - throw new InvalidCastException("Invalid json type, expected latitude/longitude object."); - } - - public object Visit(IField field) - { - return value; - } - - private object ConvertToGuidList() - { - if (value is JsonArray array) - { - var result = new List(); - - foreach (var item in array) - { - if (item is JsonScalar s && Guid.TryParse(s.Value, out var guid)) - { - result.Add(guid); - } - else - { - throw new InvalidCastException("Invalid json type, expected array of guid strings."); - } - } - - return result; - } - - throw new InvalidCastException("Invalid json type, expected array of guid strings."); - } - - private object ConvertToStringList() - { - if (value is JsonArray array) - { - var result = new List(); - - foreach (var item in array) - { - if (item is JsonNull) - { - result.Add(null); - } - else if (item is JsonScalar s) - { - result.Add(s.Value); - } - else - { - throw new InvalidCastException("Invalid json type, expected array of strings."); - } - } - - return result; - } - - throw new InvalidCastException("Invalid json type, expected array of strings."); - } - - private object ConvertToObjectList() - { - if (value is JsonArray array) - { - var result = new List(); - - foreach (var item in array) - { - if (item is JsonObject obj) - { - result.Add(obj); - } - else - { - throw new InvalidCastException("Invalid json type, expected array of objects."); - } - } - - return result; - } - - throw new InvalidCastException("Invalid json type, expected array of objects."); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs deleted file mode 100644 index a15507007..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public static class Undefined - { - public static readonly object Value = new object(); - - public static bool IsUndefined(this object other) - { - return ReferenceEquals(other, Value); - } - - public static bool IsNullOrUndefined(this object other) - { - return other == null || other.IsUndefined(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs deleted file mode 100644 index ec4740c39..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public delegate Task> CheckContents(Guid schemaId, FilterNode filter); - - public delegate Task> CheckContentsByIds(HashSet ids); - - public delegate Task> CheckAssets(IEnumerable ids); - - public sealed class ValidationContext - { - private readonly Guid contentId; - private readonly Guid schemaId; - private readonly CheckContents checkContent; - private readonly CheckContentsByIds checkContentByIds; - private readonly CheckAssets checkAsset; - private readonly ImmutableQueue propertyPath; - - public ImmutableQueue Path - { - get { return propertyPath; } - } - - public Guid ContentId - { - get { return contentId; } - } - - public Guid SchemaId - { - get { return schemaId; } - } - - public bool IsOptional { get; } - - public ValidationContext( - Guid contentId, - Guid schemaId, - CheckContents checkContent, - CheckContentsByIds checkContentsByIds, - CheckAssets checkAsset) - : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) - { - } - - private ValidationContext( - Guid contentId, - Guid schemaId, - CheckContents checkContent, - CheckContentsByIds checkContentByIds, - CheckAssets checkAsset, - ImmutableQueue propertyPath, - bool isOptional) - { - Guard.NotNull(checkAsset, nameof(checkAsset)); - Guard.NotNull(checkContent, nameof(checkContent)); - Guard.NotNull(checkContentByIds, nameof(checkContentByIds)); - - this.propertyPath = propertyPath; - - this.checkContent = checkContent; - this.checkContentByIds = checkContentByIds; - this.checkAsset = checkAsset; - this.contentId = contentId; - - this.schemaId = schemaId; - - IsOptional = isOptional; - } - - public ValidationContext Optional(bool isOptional) - { - return isOptional == IsOptional ? this : OptionalCore(isOptional); - } - - private ValidationContext OptionalCore(bool isOptional) - { - return new ValidationContext( - contentId, - schemaId, - checkContent, - checkContentByIds, - checkAsset, - propertyPath, - isOptional); - } - - public ValidationContext Nested(string property) - { - return new ValidationContext( - contentId, schemaId, - checkContent, - checkContentByIds, - checkAsset, - propertyPath.Enqueue(property), - IsOptional); - } - - public Task> GetContentIdsAsync(HashSet ids) - { - return checkContentByIds(ids); - } - - public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) - { - return checkContent(schemaId, filter); - } - - public Task> GetAssetInfosAsync(IEnumerable assetId) - { - return checkAsset(assetId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs deleted file mode 100644 index 16b842801..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class AggregateValidator : IValidator - { - private readonly IValidator[] validators; - - public AggregateValidator(IEnumerable validators) - { - this.validators = validators?.ToArray(); - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (validators?.Length > 0) - { - return Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError))); - } - - return Task.CompletedTask; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs deleted file mode 100644 index 6f0d4a63a..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class AllowedValuesValidator : IValidator - { - private readonly IEnumerable allowedValues; - - public AllowedValuesValidator(params T[] allowedValues) - : this((IEnumerable)allowedValues) - { - } - - public AllowedValuesValidator(IEnumerable allowedValues) - { - Guard.NotNull(allowedValues, nameof(allowedValues)); - - this.allowedValues = allowedValues; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value != null && value is T typedValue && !allowedValues.Contains(typedValue)) - { - addError(context.Path, "Not an allowed value."); - } - - return TaskHelper.Done; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs deleted file mode 100644 index f1a87c283..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class AssetsValidator : IValidator - { - private readonly AssetsFieldProperties properties; - - public AssetsValidator(AssetsFieldProperties properties) - { - this.properties = properties; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is ICollection assetIds && assetIds.Count > 0) - { - var assets = await context.GetAssetInfosAsync(assetIds); - var index = 0; - - foreach (var assetId in assetIds) - { - index++; - - var path = context.Path.Enqueue($"[{index}]"); - - var asset = assets.FirstOrDefault(x => x.AssetId == assetId); - - if (asset == null) - { - addError(path, $"Id '{assetId}' not found."); - continue; - } - - if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) - { - addError(path, $"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'."); - } - - if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) - { - addError(path, $"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'."); - } - - if (properties.AllowedExtensions != null && - properties.AllowedExtensions.Count > 0 && - !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) - { - addError(path, "Invalid file extension."); - } - - if (!asset.IsImage) - { - if (properties.MustBeImage) - { - addError(path, "Not an image."); - } - - continue; - } - - if (asset.PixelWidth.HasValue && - asset.PixelHeight.HasValue) - { - var w = asset.PixelWidth.Value; - var h = asset.PixelHeight.Value; - - var actualRatio = (double)w / h; - - if (properties.MinWidth.HasValue && w < properties.MinWidth) - { - addError(path, $"Width '{w}px' less than minimum of '{properties.MinWidth}px'."); - } - - if (properties.MaxWidth.HasValue && w > properties.MaxWidth) - { - addError(path, $"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'."); - } - - if (properties.MinHeight.HasValue && h < properties.MinHeight) - { - addError(path, $"Height '{h}px' less than minimum of '{properties.MinHeight}px'."); - } - - if (properties.MaxHeight.HasValue && h > properties.MaxHeight) - { - addError(path, $"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'."); - } - - if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) - { - var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; - - if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) - { - addError(path, $"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'."); - } - } - } - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs deleted file mode 100644 index 8e8efdd46..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class CollectionItemValidator : IValidator - { - private readonly IValidator[] itemValidators; - - public CollectionItemValidator(params IValidator[] itemValidators) - { - Guard.NotNull(itemValidators, nameof(itemValidators)); - Guard.NotEmpty(itemValidators, nameof(itemValidators)); - - this.itemValidators = itemValidators; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is ICollection items && items.Count > 0) - { - var innerTasks = new List(); - var index = 1; - - foreach (var item in items) - { - var innerContext = context.Nested($"[{index}]"); - - foreach (var itemValidator in itemValidators) - { - innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, addError)); - } - - index++; - } - - await Task.WhenAll(innerTasks); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs deleted file mode 100644 index a858b811c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class CollectionValidator : IValidator - { - private readonly bool isRequired; - private readonly int? minItems; - private readonly int? maxItems; - - public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) - { - if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value) - { - throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); - } - - this.isRequired = isRequired; - this.minItems = minItems; - this.maxItems = maxItems; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (!(value is ICollection items) || items.Count == 0) - { - if (isRequired && !context.IsOptional) - { - addError(context.Path, "Field is required."); - } - - return TaskHelper.Done; - } - - if (minItems.HasValue && maxItems.HasValue) - { - if (minItems == maxItems && minItems != items.Count) - { - addError(context.Path, $"Must have exactly {maxItems} item(s)."); - } - else if (items.Count < minItems || items.Count > maxItems) - { - addError(context.Path, $"Must have between {minItems} and {maxItems} item(s)."); - } - } - else - { - if (minItems.HasValue && items.Count < minItems.Value) - { - addError(context.Path, $"Must have at least {minItems} item(s)."); - } - - if (maxItems.HasValue && items.Count > maxItems.Value) - { - addError(context.Path, $"Must not have more than {maxItems} item(s)."); - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs deleted file mode 100644 index 8f2f2689c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class FieldValidator : IValidator - { - private readonly IValidator[] validators; - private readonly IField field; - - public FieldValidator(IEnumerable validators, IField field) - { - Guard.NotNull(field, nameof(field)); - - this.validators = validators.ToArray(); - - this.field = field; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - try - { - var typedValue = value; - - if (value is IJsonValue jsonValue) - { - if (jsonValue.Type == JsonValueType.Null) - { - typedValue = null; - } - else - { - typedValue = JsonValueConverter.ConvertValue(field, jsonValue); - } - } - - if (validators?.Length > 0) - { - var tasks = new List(); - - foreach (var validator in validators) - { - tasks.Add(validator.ValidateAsync(typedValue, context, addError)); - } - - await Task.WhenAll(tasks); - } - } - catch - { - addError(context.Path, "Not a valid value."); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs deleted file mode 100644 index 47592700f..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public delegate void AddError(IEnumerable path, string message); - - public interface IValidator - { - Task ValidateAsync(object value, ValidationContext context, AddError addError); - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs deleted file mode 100644 index 835a10d31..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class NoValueValidator : IValidator - { - public static readonly NoValueValidator Instance = new NoValueValidator(); - - private NoValueValidator() - { - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (!value.IsUndefined()) - { - addError(context.Path, "Value must not be defined."); - } - - return Task.CompletedTask; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs deleted file mode 100644 index c86c85be3..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class ObjectValidator : IValidator - { - private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); - private readonly IDictionary schema; - private readonly bool isPartial; - private readonly string fieldType; - - public ObjectValidator(IDictionary schema, bool isPartial, string fieldType) - { - this.schema = schema; - this.fieldType = fieldType; - this.isPartial = isPartial; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value.IsNullOrUndefined()) - { - value = DefaultValue; - } - - if (value is IReadOnlyDictionary values) - { - foreach (var fieldData in values) - { - var name = fieldData.Key; - - if (!schema.ContainsKey(name)) - { - addError(context.Path.Enqueue(name), $"Not a known {fieldType}."); - } - } - - var tasks = new List(); - - foreach (var field in schema) - { - var name = field.Key; - - var (isOptional, validator) = field.Value; - - var fieldValue = Undefined.Value; - - if (!values.TryGetValue(name, out var temp)) - { - if (isPartial) - { - continue; - } - } - else - { - fieldValue = temp; - } - - var fieldContext = context.Nested(name).Optional(isOptional); - - tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError)); - } - - await Task.WhenAll(tasks); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs deleted file mode 100644 index af952f7fb..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class PatternValidator : IValidator - { - private static readonly TimeSpan Timeout = TimeSpan.FromMilliseconds(20); - private readonly Regex regex; - private readonly string errorMessage; - - public PatternValidator(string pattern, string errorMessage = null) - { - this.errorMessage = errorMessage; - - regex = new Regex("^" + pattern + "$", RegexOptions.None, Timeout); - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is string stringValue) - { - if (!string.IsNullOrEmpty(stringValue)) - { - try - { - if (!regex.IsMatch(stringValue)) - { - if (string.IsNullOrWhiteSpace(errorMessage)) - { - addError(context.Path, "Does not match to the pattern."); - } - else - { - addError(context.Path, errorMessage); - } - } - } - catch - { - addError(context.Path, "Regex is too slow."); - } - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs deleted file mode 100644 index 2209786f7..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class RangeValidator : IValidator where T : struct, IComparable - { - private readonly T? min; - private readonly T? max; - - public RangeValidator(T? min, T? max) - { - if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) > 0) - { - throw new ArgumentException("Min value must be greater than max value.", nameof(min)); - } - - this.min = min; - this.max = max; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value != null && value is T typedValue) - { - if (min.HasValue && max.HasValue) - { - if (Equals(min, max) && Equals(min.Value, max.Value)) - { - addError(context.Path, $"Must be exactly '{max}'."); - } - else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) - { - addError(context.Path, $"Must be between '{min}' and '{max}'."); - } - } - else - { - if (min.HasValue && typedValue.CompareTo(min.Value) < 0) - { - addError(context.Path, $"Must be greater or equal to '{min}'."); - } - - if (max.HasValue && typedValue.CompareTo(max.Value) > 0) - { - addError(context.Path, $"Must be less or equal to '{max}'."); - } - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs deleted file mode 100644 index 62ad9a34c..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class ReferencesValidator : IValidator - { - private readonly IEnumerable schemaIds; - - public ReferencesValidator(IEnumerable schemaIds) - { - this.schemaIds = schemaIds; - } - - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is ICollection contentIds) - { - var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); - - foreach (var id in contentIds) - { - var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id); - - if (schemaId == Guid.Empty) - { - addError(context.Path, $"Contains invalid reference '{id}'."); - } - else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId)) - { - addError(context.Path, $"Contains reference '{id}' to invalid schema."); - } - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs deleted file mode 100644 index 129f88dab..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class RequiredStringValidator : IValidator - { - private readonly bool validateEmptyStrings; - - public RequiredStringValidator(bool validateEmptyStrings = false) - { - this.validateEmptyStrings = validateEmptyStrings; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (context.IsOptional) - { - return TaskHelper.Done; - } - - if (value.IsNullOrUndefined() || IsEmptyString(value)) - { - addError(context.Path, "Field is required."); - } - - return TaskHelper.Done; - } - - private bool IsEmptyString(object value) - { - return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs deleted file mode 100644 index 6a92a3671..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class RequiredValidator : IValidator - { - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value.IsNullOrUndefined() && !context.IsOptional) - { - addError(context.Path, "Field is required."); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs deleted file mode 100644 index b6579a7bc..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public class StringLengthValidator : IValidator - { - private readonly int? minLength; - private readonly int? maxLength; - - public StringLengthValidator(int? minLength, int? maxLength) - { - if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value) - { - throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); - } - - this.minLength = minLength; - this.maxLength = maxLength; - } - - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) - { - if (minLength.HasValue && maxLength.HasValue) - { - if (minLength == maxLength && minLength != stringValue.Length) - { - addError(context.Path, $"Must have exactly {maxLength} character(s)."); - } - else if (stringValue.Length < minLength || stringValue.Length > maxLength) - { - addError(context.Path, $"Must have between {minLength} and {maxLength} character(s)."); - } - } - else - { - if (minLength.HasValue && stringValue.Length < minLength.Value) - { - addError(context.Path, $"Must have at least {minLength} character(s)."); - } - - if (maxLength.HasValue && stringValue.Length > maxLength.Value) - { - addError(context.Path, $"Must not have more than {maxLength} character(s)."); - } - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs deleted file mode 100644 index 6fad491b9..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class UniqueValidator : IValidator - { - public async Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - var count = context.Path.Count(); - - if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) - { - FilterNode filter = null; - - if (value is string s) - { - filter = ClrFilter.Eq(Path(context), s); - } - else if (value is double d) - { - filter = ClrFilter.Eq(Path(context), d); - } - - if (filter != null) - { - var found = await context.GetContentIdsAsync(context.SchemaId, filter); - - if (found.Any(x => x.Id != context.ContentId)) - { - addError(context.Path, "Another content with the same value exists."); - } - } - } - } - - private static List Path(ValidationContext context) - { - return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs deleted file mode 100644 index 7c948165b..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public sealed class UniqueValuesValidator : IValidator - { - public Task ValidateAsync(object value, ValidationContext context, AddError addError) - { - if (value is IEnumerable items && items.Any()) - { - var itemsArray = items.ToArray(); - - if (itemsArray.Length != itemsArray.Distinct().Count()) - { - addError(context.Path, "Must not contain duplicate values."); - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs deleted file mode 100644 index 800db6550..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ========================================================================== -// 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 System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets -{ - public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository - { - public MongoAssetRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Assets"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Tags) - .Descending(x => x.LastModified)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Slug)) - }, ct); - } - - public async Task> QueryAsync(Guid appId, ClrQuery query) - { - using (Profiler.TraceMethod("QueryAsyncByQuery")) - { - try - { - query = query.AdjustToModel(); - - var filter = query.BuildFilter(appId); - - var contentCount = Collection.Find(filter).CountDocumentsAsync(); - var contentItems = - Collection.Find(filter) - .AssetTake(query) - .AssetSkip(query) - .AssetSort(query) - .ToListAsync(); - - await Task.WhenAll(contentItems, contentCount); - - return ResultList.Create(contentCount.Result, contentItems.Result); - } - catch (MongoQueryException ex) - { - if (ex.Message.Contains("17406")) - { - throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); - } - else - { - throw; - } - } - } - } - - public async Task> QueryAsync(Guid appId, HashSet ids) - { - using (Profiler.TraceMethod("QueryAsyncByIds")) - { - var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified); - - var assetItems = await find.ToListAsync(); - - return ResultList.Create(assetItems.Count, assetItems.OfType()); - } - } - - public async Task FindAssetBySlugAsync(Guid appId, string slug) - { - using (Profiler.TraceMethod()) - { - var assetEntity = - await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.Slug == slug) - .FirstOrDefaultAsync(); - - return assetEntity; - } - } - - public async Task> QueryByHashAsync(Guid appId, string hash) - { - using (Profiler.TraceMethod()) - { - var assetEntities = - await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash) - .ToListAsync(); - - return assetEntities.OfType().ToList(); - } - } - - public async Task FindAssetAsync(Guid id, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var assetEntity = - await Collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); - - if (assetEntity?.IsDeleted == true && !allowDeleted) - { - return null; - } - - return assetEntity; - } - } - - public Task RemoveAsync(Guid key) - { - return Collection.DeleteOneAsync(x => x.Id == key); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs deleted file mode 100644 index 0b4828446..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets -{ - public sealed partial class MongoAssetRepository : ISnapshotStore - { - async Task<(AssetState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (Map(existing), existing.Version); - } - - return (null, EtagVersion.NotFound); - } - } - - async Task ISnapshotStore.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) - { - using (Profiler.TraceMethod()) - { - var entity = SimpleMapper.Map(value, new MongoAssetEntity()); - - entity.Version = newVersion; - entity.IndexedAppId = value.AppId.Id; - - await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); - } - } - - async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) - { - using (Profiler.TraceMethod()) - { - await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct); - } - } - - async Task ISnapshotStore.RemoveAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - await Collection.DeleteOneAsync(x => x.Id == key); - } - } - - private static AssetState Map(MongoAssetEntity existing) - { - return SimpleMapper.Map(existing, new AssetState()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs deleted file mode 100644 index 7988b72a7..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ /dev/null @@ -1,271 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - internal class MongoContentCollection : MongoRepositoryBase - { - private readonly IAppProvider appProvider; - private readonly IJsonSerializer serializer; - - public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) - : base(database) - { - this.appProvider = appProvider; - - this.serializer = serializer; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.ScheduledAt) - .Ascending(x => x.IsDeleted)), - new CreateIndexModel(Index - .Ascending(x => x.ReferencedIds)) - }, ct); - } - - protected override string CollectionName() - { - return "State_Contents"; - } - - public async Task> QueryAsync(ISchemaEntity schema, ClrQuery query, List ids, Status[] status, bool inDraft, bool includeDraft = true) - { - try - { - query = query.AdjustToModel(schema.SchemaDef, inDraft); - - var filter = query.ToFilter(schema.Id, ids, status); - - var contentCount = Collection.Find(filter).CountDocumentsAsync(); - var contentItems = - Collection.Find(filter) - .WithoutDraft(includeDraft) - .ContentTake(query) - .ContentSkip(query) - .ContentSort(query) - .ToListAsync(); - - await Task.WhenAll(contentItems, contentCount); - - foreach (var entity in contentItems.Result) - { - entity.ParseData(schema.SchemaDef, serializer); - } - - return ResultList.Create(contentCount.Result, contentItems.Result); - } - catch (MongoQueryException ex) - { - if (ex.Message.Contains("17406")) - { - throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); - } - else - { - throw; - } - } - } - - public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status, bool includeDraft) - { - var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); - - var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); - - var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); - var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); - - var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); - - foreach (var entity in contentItems) - { - var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId); - - if (schema != null) - { - entity.ParseData(schema.SchemaDef, serializer); - - result.Add((entity, schema)); - } - } - - return result; - } - - public async Task> QueryAsync(ISchemaEntity schema, HashSet ids, Status[] status, bool includeDraft) - { - var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); - - var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); - - foreach (var entity in contentItems) - { - entity.ParseData(schema.SchemaDef, serializer); - } - - return ResultList.Create(contentItems.Count, contentItems); - } - - public async Task FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft) - { - var find = Collection.Find(FilterFactory.Build(schema.Id, id, status)); - - var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync(); - - contentEntity?.ParseData(schema.SchemaDef, serializer); - - return contentEntity; - } - - public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) - { - return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) - .Not(x => x.DataByIds) - .Not(x => x.DataDraftByIds) - .ForEachAsync(c => - { - callback(c); - }); - } - - public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) - { - var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); - - var contentEntities = - await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) - .ToListAsync(); - - return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); - } - - public async Task> QueryIdsAsync(HashSet ids) - { - var contentEntities = - await Collection.Find(Filter.In(x => x.Id, ids)).Only(x => x.Id, x => x.IndexedSchemaId) - .ToListAsync(); - - return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); - } - - public async Task> QueryIdsAsync(Guid appId) - { - var contentEntities = - await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) - .ToListAsync(); - - return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema) - { - var contentEntity = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (contentEntity != null) - { - var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - - contentEntity.ParseData(schema.SchemaDef, serializer); - - return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task ReadAllAsync(Func callback, Func> getSchema, CancellationToken ct = default) - { - return Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(async contentEntity => - { - var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - - contentEntity.ParseData(schema.SchemaDef, serializer); - - await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); - }, ct); - } - - public Task CleanupAsync(Guid id) - { - return Collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, id), - Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), - Update.AddToSet(x => x.ReferencedIdsDeleted, id)); - } - - public Task RemoveAsync(Guid id) - { - return Collection.DeleteOneAsync(x => x.Id == id); - } - - public async Task UpsertAsync(MongoContentEntity content, long oldVersion) - { - try - { - await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs deleted file mode 100644 index fe2e0649c..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public sealed class MongoContentEntity : IContentEntity - { - private NamedContentData data; - private NamedContentData dataDraft; - - [BsonId] - [BsonElement("_id")] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonRequired] - [BsonElement("_ai")] - [BsonRepresentation(BsonType.String)] - public Guid IndexedAppId { get; set; } - - [BsonRequired] - [BsonElement("_si")] - [BsonRepresentation(BsonType.String)] - public Guid IndexedSchemaId { get; set; } - - [BsonRequired] - [BsonElement("rf")] - [BsonRepresentation(BsonType.String)] - public List ReferencedIds { get; set; } - - [BsonRequired] - [BsonElement("rd")] - [BsonRepresentation(BsonType.String)] - public List ReferencedIdsDeleted { get; set; } = new List(); - - [BsonRequired] - [BsonElement("ss")] - public Status Status { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("do")] - [BsonJson] - public IdContentData DataByIds { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("dd")] - [BsonJson] - public IdContentData DataDraftByIds { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("sj")] - [BsonJson] - public ScheduleJob ScheduleJob { get; set; } - - [BsonRequired] - [BsonElement("ai")] - public NamedId AppId { get; set; } - - [BsonRequired] - [BsonElement("si")] - public NamedId SchemaId { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("sa")] - public Instant? ScheduledAt { get; set; } - - [BsonRequired] - [BsonElement("ct")] - public Instant Created { get; set; } - - [BsonRequired] - [BsonElement("mt")] - public Instant LastModified { get; set; } - - [BsonRequired] - [BsonElement("vs")] - public long Version { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("dl")] - public bool IsDeleted { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("pd")] - public bool IsPending { get; set; } - - [BsonRequired] - [BsonElement("cb")] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement("mb")] - public RefToken LastModifiedBy { get; set; } - - [BsonIgnore] - public NamedContentData Data - { - get { return data; } - } - - [BsonIgnore] - public NamedContentData DataDraft - { - get { return dataDraft; } - } - - public void ParseData(Schema schema, IJsonSerializer serializer) - { - data = DataByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer); - - if (DataDraftByIds != null) - { - dataDraft = DataDraftByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs deleted file mode 100644 index c7d13da72..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ========================================================================== -// 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.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Text; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public partial class MongoContentRepository : IContentRepository, IInitializable - { - private readonly IAppProvider appProvider; - private readonly IJsonSerializer serializer; - private readonly ITextIndexer indexer; - private readonly string typeAssetDeleted; - private readonly string typeContentDeleted; - private readonly MongoContentCollection contents; - - static MongoContentRepository() - { - StatusSerializer.Register(); - } - - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(indexer, nameof(indexer)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.appProvider = appProvider; - this.indexer = indexer; - this.serializer = serializer; - - typeAssetDeleted = typeNameRegistry.GetName(); - typeContentDeleted = typeNameRegistry.GetName(); - - contents = new MongoContentCollection(database, serializer, appProvider); - } - - public Task InitializeAsync(CancellationToken ct = default) - { - return contents.InitializeAsync(ct); - } - - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(query, nameof(query)); - - using (Profiler.TraceMethod("QueryAsyncByQuery")) - { - var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); - - if (fullTextIds?.Count == 0) - { - return ResultList.CreateFrom(0); - } - - return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft); - } - } - - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(ids, nameof(ids)); - Guard.NotNull(schema, nameof(schema)); - - using (Profiler.TraceMethod("QueryAsyncByIds")) - { - return await contents.QueryAsync(schema, ids, status, includeDraft); - } - } - - public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(ids, nameof(ids)); - - using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) - { - return await contents.QueryAsync(app, ids, status, includeDraft); - } - } - - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft = true) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(schema, nameof(schema)); - - using (Profiler.TraceMethod()) - { - return await contents.FindContentAsync(schema, id, status, includeDraft); - } - } - - public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) - { - using (Profiler.TraceMethod()) - { - return await contents.QueryIdsAsync(await appProvider.GetSchemaAsync(appId, schemaId), filterNode); - } - } - - public async Task> QueryIdsAsync(Guid appId, HashSet ids) - { - using (Profiler.TraceMethod()) - { - return await contents.QueryIdsAsync(ids); - } - } - - public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback) - { - using (Profiler.TraceMethod()) - { - await contents.QueryScheduledWithoutDataAsync(now, callback); - } - } - - public Task ClearAsync() - { - return contents.ClearAsync(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs deleted file mode 100644 index 67aa7e213..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public partial class MongoContentRepository : ISnapshotStore - { - async Task ISnapshotStore.RemoveAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - await contents.RemoveAsync(key); - } - } - - async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) - { - using (Profiler.TraceMethod()) - { - await contents.ReadAllAsync(callback, GetSchemaAsync, ct); - } - } - - async Task<(ContentState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) - { - using (Profiler.TraceMethod()) - { - return await contents.ReadAsync(key, GetSchemaAsync); - } - } - - async Task ISnapshotStore.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) - { - using (Profiler.TraceMethod()) - { - if (value.SchemaId.Id == Guid.Empty) - { - return; - } - - var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id); - - var idData = value.Data.ToMongoModel(schema.SchemaDef, serializer); - var idDraftData = idData; - - if (!ReferenceEquals(value.Data, value.DataDraft)) - { - idDraftData = value.DataDraft?.ToMongoModel(schema.SchemaDef, serializer); - } - - var content = SimpleMapper.Map(value, new MongoContentEntity - { - DataByIds = idData, - DataDraftByIds = idDraftData, - IsDeleted = value.IsDeleted, - IndexedAppId = value.AppId.Id, - IndexedSchemaId = value.SchemaId.Id, - ReferencedIds = idData.ToReferencedIds(schema.SchemaDef), - ScheduledAt = value.ScheduleJob?.DueTime, - Version = newVersion - }); - - await contents.UpsertAsync(content, oldVersion); - } - } - - private async Task GetSchemaAsync(Guid appId, Guid schemaId) - { - var schema = await appProvider.GetSchemaAsync(appId, schemaId, true); - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(ISchemaEntity)); - } - - return schema; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs deleted file mode 100644 index c43523d61..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// 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 MongoDB.Driver; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors -{ - public static class FilterFactory - { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - - public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft) - { - var pathConverter = Adapt.Path(schema, useDraft); - - if (query.Filter != null) - { - query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter)); - } - - query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList(); - - return query; - } - - public static FilterNode AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft) - { - var pathConverter = Adapt.Path(schema, useDraft); - - return filterNode.Accept(new AdaptionVisitor(pathConverter)); - } - - public static IFindFluent ContentSort(this IFindFluent cursor, ClrQuery query) - { - return cursor.Sort(query.BuildSort()); - } - - public static IFindFluent ContentTake(this IFindFluent cursor, ClrQuery query) - { - return cursor.Take(query); - } - - public static IFindFluent ContentSkip(this IFindFluent cursor, ClrQuery query) - { - return cursor.Skip(query); - } - - public static IFindFluent WithoutDraft(this IFindFluent cursor, bool includeDraft) - { - return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; - } - - public static FilterDefinition Build(Guid schemaId, Guid id, Status[] status) - { - return CreateFilter(null, schemaId, new List { id }, status, null); - } - - public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[] status) - { - return CreateFilter(appId, null, ids, status, null); - } - - public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[] status) - { - return CreateFilter(null, schemaId, ids, status, null); - } - - public static FilterDefinition ToFilter(this ClrQuery query, Guid schemaId, ICollection ids, Status[] status) - { - return CreateFilter(null, schemaId, ids, status, query); - } - - private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection ids, Status[] status, - ClrQuery query) - { - var filters = new List>(); - - if (appId.HasValue) - { - filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); - } - - if (schemaId.HasValue) - { - filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); - } - - filters.Add(Filter.Ne(x => x.IsDeleted, true)); - - if (status != null) - { - filters.Add(Filter.In(x => x.Status, status)); - } - - if (ids != null && ids.Count > 0) - { - if (ids.Count > 1) - { - filters.Add(Filter.In(x => x.Id, ids)); - } - else - { - filters.Add(Filter.Eq(x => x.Id, ids.First())); - } - } - - if (query?.Filter != null) - { - filters.Add(query.Filter.BuildFilter()); - } - - return Filter.And(filters); - } - - public static FilterDefinition ToFilter(this FilterNode filterNode, Guid schemaId) - { - var filters = new List> - { - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true), - filterNode.BuildFilter() - }; - - return Filter.And(filters); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs deleted file mode 100644 index 7f9c094f2..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity - { - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid RuleId { get; set; } - - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public RuleResult Result { get; set; } - - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public RuleJobResult JobResult { get; set; } - - [BsonRequired] - [BsonElement] - [BsonJson] - public RuleJob Job { get; set; } - - [BsonRequired] - [BsonElement] - public string LastDump { get; set; } - - [BsonRequired] - [BsonElement] - public int NumCalls { get; set; } - - [BsonRequired] - [BsonElement] - public Instant Expires { get; set; } - - [BsonRequired] - [BsonElement] - public Instant? NextAttempt { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs deleted file mode 100644 index fed408874..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// 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.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository - { - private readonly MongoRuleStatisticsCollection statisticsCollection; - - public MongoRuleEventRepository(IMongoDatabase database) - : base(database) - { - statisticsCollection = new MongoRuleStatisticsCollection(database); - } - - protected override string CollectionName() - { - return "RuleEvents"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - await statisticsCollection.InitializeAsync(ct); - - await collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel(Index.Ascending(x => x.NextAttempt)), - new CreateIndexModel(Index.Ascending(x => x.AppId).Descending(x => x.Created)), - new CreateIndexModel( - Index - .Ascending(x => x.Expires), - new CreateIndexOptions - { - ExpireAfter = TimeSpan.Zero - }) - }, ct); - } - - public Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default) - { - return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); - } - - public async Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20) - { - var filter = Filter.Eq(x => x.AppId, appId); - - if (ruleId.HasValue) - { - filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId)); - } - - var ruleEventEntities = - await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created) - .ToListAsync(); - - return ruleEventEntities; - } - - public async Task FindAsync(Guid id) - { - var ruleEvent = - await Collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); - - return ruleEvent; - } - - public async Task CountByAppAsync(Guid appId) - { - return (int)await Collection.CountDocumentsAsync(x => x.AppId == appId); - } - - public Task EnqueueAsync(Guid id, Instant nextAttempt) - { - return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); - } - - public Task EnqueueAsync(RuleJob job, Instant nextAttempt) - { - var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); - - return Collection.InsertOneIfNotExistsAsync(entity); - } - - public Task CancelAsync(Guid id) - { - return Collection.UpdateOneAsync(x => x.Id == id, - Update - .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled)); - } - - public async Task MarkSentAsync(RuleJob job, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall) - { - if (result == RuleResult.Success) - { - await statisticsCollection.IncrementSuccess(job.AppId, job.RuleId, finished); - } - else - { - await statisticsCollection.IncrementFailed(job.AppId, job.RuleId, finished); - } - - await Collection.UpdateOneAsync(x => x.Id == job.Id, - Update - .Set(x => x.Result, result) - .Set(x => x.LastDump, dump) - .Set(x => x.JobResult, jobResult) - .Set(x => x.NextAttempt, nextCall) - .Inc(x => x.NumCalls, 1)); - } - - public Task> QueryStatisticsByAppAsync(Guid appId) - { - return statisticsCollection.QueryByAppAsync(appId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj deleted file mode 100644 index 1b63c901a..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs deleted file mode 100644 index f4b76508f..000000000 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Entities -{ - public sealed class AppProvider : IAppProvider - { - private readonly ILocalCache localCache; - private readonly IAppsIndex indexForApps; - private readonly IRulesIndex indexRules; - private readonly ISchemasIndex indexSchemas; - - public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas) - { - Guard.NotNull(indexForApps, nameof(indexForApps)); - Guard.NotNull(indexRules, nameof(indexRules)); - Guard.NotNull(indexSchemas, nameof(indexSchemas)); - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; - this.indexForApps = indexForApps; - this.indexRules = indexRules; - this.indexSchemas = indexSchemas; - } - - public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) - { - return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => - { - var app = await GetAppAsync(appId); - - if (app == null) - { - return (null, null); - } - - var schema = await GetSchemaAsync(appId, id, false); - - if (schema == null) - { - return (null, null); - } - - return (app, schema); - }); - } - - public Task GetAppAsync(Guid appId) - { - return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () => - { - return await indexForApps.GetAppAsync(appId); - }); - } - - public Task GetAppAsync(string appName) - { - return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => - { - return await indexForApps.GetAppByNameAsync(appName); - }); - } - - public Task> GetUserAppsAsync(string userId, PermissionSet permissions) - { - return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => - { - return await indexForApps.GetAppsForUserAsync(userId, permissions); - }); - } - - public Task GetSchemaAsync(Guid appId, string name) - { - return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => - { - return await indexSchemas.GetSchemaByNameAsync(appId, name); - }); - } - - public Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) - { - return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => - { - return await indexSchemas.GetSchemaAsync(appId, id, allowDeleted); - }); - } - - public Task> GetSchemasAsync(Guid appId) - { - return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => - { - return await indexSchemas.GetSchemasAsync(appId); - }); - } - - public Task> GetRulesAsync(Guid appId) - { - return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => - { - return await indexRules.GetRulesAsync(appId); - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs deleted file mode 100644 index f1caac4dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppCommandMiddleware : GrainCommandMiddleware - { - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IContextProvider contextProvider; - - public AppCommandMiddleware( - IGrainFactory grainFactory, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IContextProvider contextProvider) - : base(grainFactory) - { - Guard.NotNull(contextProvider, nameof(contextProvider)); - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); - - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.contextProvider = contextProvider; - } - - public override async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is UploadAppImage uploadImage) - { - await UploadAsync(uploadImage); - } - - await ExecuteCommandAsync(context); - - if (context.PlainResult is IAppEntity app) - { - contextProvider.Context.App = app; - } - - await next(); - } - - private async Task UploadAsync(UploadAppImage uploadImage) - { - var file = uploadImage.File; - - var image = await assetThumbnailGenerator.GetImageInfoAsync(file.OpenRead()); - - if (image == null) - { - throw new ValidationException("File is not an image."); - } - - await assetStore.UploadAsync(uploadImage.AppId.ToString(), file.OpenRead(), true); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs deleted file mode 100644 index b7ab61cf5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ /dev/null @@ -1,510 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Guards; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppGrain : DomainObjectGrain, IAppGrain - { - private readonly InitialPatterns initialPatterns; - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; - private readonly IUserResolver userResolver; - - public AppGrain( - InitialPatterns initialPatterns, - IStore store, - ISemanticLog log, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager, - IUserResolver userResolver) - : base(store, log) - { - Guard.NotNull(initialPatterns, nameof(initialPatterns)); - Guard.NotNull(userResolver, nameof(userResolver)); - Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); - Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); - - this.userResolver = userResolver; - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; - this.initialPatterns = initialPatterns; - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotArchived(); - - switch (command) - { - case CreateApp createApp: - return CreateReturn(createApp, c => - { - GuardApp.CanCreate(c); - - Create(c); - - return Snapshot; - }); - - case UpdateApp updateApp: - return UpdateReturn(updateApp, c => - { - GuardApp.CanUpdate(c); - - Update(c); - - return Snapshot; - }); - - case UploadAppImage uploadImage: - return UpdateReturn(uploadImage, c => - { - GuardApp.CanUploadImage(c); - - UploadImage(c); - - return Snapshot; - }); - - case RemoveAppImage removeImage: - return UpdateReturn(removeImage, c => - { - GuardApp.CanRemoveImage(c); - - RemoveImage(c); - - return Snapshot; - }); - - case AssignContributor assignContributor: - return UpdateReturnAsync(assignContributor, async c => - { - await GuardAppContributors.CanAssign(Snapshot.Contributors, Snapshot.Roles, c, userResolver, GetPlan()); - - AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); - - return Snapshot; - }); - - case RemoveContributor removeContributor: - return UpdateReturn(removeContributor, c => - { - GuardAppContributors.CanRemove(Snapshot.Contributors, c); - - RemoveContributor(c); - - return Snapshot; - }); - - case AttachClient attachClient: - return UpdateReturn(attachClient, c => - { - GuardAppClients.CanAttach(Snapshot.Clients, c); - - AttachClient(c); - - return Snapshot; - }); - - case UpdateClient updateClient: - return UpdateReturn(updateClient, c => - { - GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles); - - UpdateClient(c); - - return Snapshot; - }); - - case RevokeClient revokeClient: - return UpdateReturn(revokeClient, c => - { - GuardAppClients.CanRevoke(Snapshot.Clients, c); - - RevokeClient(c); - - return Snapshot; - }); - - case AddWorkflow addWorkflow: - return UpdateReturn(addWorkflow, c => - { - GuardAppWorkflows.CanAdd(c); - - AddWorkflow(c); - - return Snapshot; - }); - - case UpdateWorkflow updateWorkflow: - return UpdateReturn(updateWorkflow, c => - { - GuardAppWorkflows.CanUpdate(Snapshot.Workflows, c); - - UpdateWorkflow(c); - - return Snapshot; - }); - - case DeleteWorkflow deleteWorkflow: - return UpdateReturn(deleteWorkflow, c => - { - GuardAppWorkflows.CanDelete(Snapshot.Workflows, c); - - DeleteWorkflow(c); - - return Snapshot; - }); - - case AddLanguage addLanguage: - return UpdateReturn(addLanguage, c => - { - GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c); - - AddLanguage(c); - - return Snapshot; - }); - - case RemoveLanguage removeLanguage: - return UpdateReturn(removeLanguage, c => - { - GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c); - - RemoveLanguage(c); - - return Snapshot; - }); - - case UpdateLanguage updateLanguage: - return UpdateReturn(updateLanguage, c => - { - GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c); - - UpdateLanguage(c); - - return Snapshot; - }); - - case AddRole addRole: - return UpdateReturn(addRole, c => - { - GuardAppRoles.CanAdd(Snapshot.Roles, c); - - AddRole(c); - - return Snapshot; - }); - - case DeleteRole deleteRole: - return UpdateReturn(deleteRole, c => - { - GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients); - - DeleteRole(c); - - return Snapshot; - }); - - case UpdateRole updateRole: - return UpdateReturn(updateRole, c => - { - GuardAppRoles.CanUpdate(Snapshot.Roles, c); - - UpdateRole(c); - - return Snapshot; - }); - - case AddPattern addPattern: - return UpdateReturn(addPattern, c => - { - GuardAppPatterns.CanAdd(Snapshot.Patterns, c); - - AddPattern(c); - - return Snapshot; - }); - - case DeletePattern deletePattern: - return UpdateReturn(deletePattern, c => - { - GuardAppPatterns.CanDelete(Snapshot.Patterns, c); - - DeletePattern(c); - - return Snapshot; - }); - - case UpdatePattern updatePattern: - return UpdateReturn(updatePattern, c => - { - GuardAppPatterns.CanUpdate(Snapshot.Patterns, c); - - UpdatePattern(c); - - return Snapshot; - }); - - case ChangePlan changePlan: - return UpdateReturnAsync(changePlan, async c => - { - GuardApp.CanChangePlan(c, Snapshot.Plan, appPlansProvider); - - if (c.FromCallback) - { - ChangePlan(c); - - return null; - } - else - { - var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), c.PlanId); - - switch (result) - { - case PlanChangedResult _: - ChangePlan(c); - break; - case PlanResetResult _: - ResetPlan(c); - break; - } - - return result; - } - }); - - case ArchiveApp archiveApp: - return UpdateAsync(archiveApp, async c => - { - await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null); - - ArchiveApp(c); - }); - - default: - throw new NotSupportedException(); - } - } - - private IAppLimitsPlan GetPlan() - { - return appPlansProvider.GetPlan(Snapshot.Plan?.PlanId); - } - - public void Create(CreateApp command) - { - var appId = NamedId.Of(command.AppId, command.Name); - - var events = new List - { - CreateInitalEvent(command.Name), - CreateInitialOwner(command.Actor), - CreateInitialLanguage() - }; - - foreach (var pattern in initialPatterns) - { - events.Add(CreateInitialPattern(pattern.Key, pattern.Value)); - } - - foreach (var @event in events) - { - @event.Actor = command.Actor; - @event.AppId = appId; - - RaiseEvent(@event); - } - } - - public void UpdateClient(UpdateClient command) - { - if (!string.IsNullOrWhiteSpace(command.Name)) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - } - - if (command.Role != null) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role })); - } - } - - public void Update(UpdateApp command) - { - RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); - } - - public void UploadImage(UploadAppImage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); - } - - public void RemoveImage(RemoveAppImage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppImageRemoved())); - } - - public void UpdateLanguage(UpdateLanguage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); - } - - public void AssignContributor(AssignContributor command, bool isAdded) - { - RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { IsAdded = isAdded })); - } - - public void RemoveContributor(RemoveContributor command) - { - RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); - } - - public void AttachClient(AttachClient command) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); - } - - public void RevokeClient(RevokeClient command) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); - } - - public void AddWorkflow(AddWorkflow command) - { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); - } - - public void UpdateWorkflow(UpdateWorkflow command) - { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); - } - - public void DeleteWorkflow(DeleteWorkflow command) - { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); - } - - public void AddLanguage(AddLanguage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); - } - - public void RemoveLanguage(RemoveLanguage command) - { - RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); - } - - public void ChangePlan(ChangePlan command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); - } - - public void ResetPlan(ChangePlan command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPlanReset())); - } - - public void AddPattern(AddPattern command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); - } - - public void DeletePattern(DeletePattern command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); - } - - public void UpdatePattern(UpdatePattern command) - { - RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); - } - - public void AddRole(AddRole command) - { - RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded())); - } - - public void DeleteRole(DeleteRole command) - { - RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted())); - } - - public void UpdateRole(UpdateRole command) - { - RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated())); - } - - public void ArchiveApp(ArchiveApp command) - { - RaiseEvent(SimpleMapper.Map(command, new AppArchived())); - } - - private void VerifyNotArchived() - { - if (Snapshot.IsArchived) - { - throw new DomainException("App has already been archived."); - } - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = NamedId.Of(Snapshot.Id, Snapshot.Name); - } - - RaiseEvent(Envelope.Create(@event)); - } - - private static AppCreated CreateInitalEvent(string name) - { - return new AppCreated { Name = name }; - } - - private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern) - { - return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message }; - } - - private static AppLanguageAdded CreateInitialLanguage() - { - return new AppLanguageAdded { Language = Language.EN }; - } - - private static AppContributorAssigned CreateInitialOwner(RefToken actor) - { - return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; - } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs deleted file mode 100644 index d52434575..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ /dev/null @@ -1,161 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppHistoryEventsCreator : HistoryEventsCreatorBase - { - public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "assigned {user:[Contributor]} as {[Role]}"); - - AddEventMessage( - "removed {user:[Contributor]} from app"); - - AddEventMessage( - "added client {[Id]} to app"); - - AddEventMessage( - "revoked client {[Id]}"); - - AddEventMessage( - "updated client {[Id]}"); - - AddEventMessage( - "renamed client {[Id]} to {[Name]}"); - - AddEventMessage( - "changed plan to {[Plan]}"); - - AddEventMessage( - "resetted plan"); - - AddEventMessage( - "added language {[Language]}"); - - AddEventMessage( - "removed language {[Language]}"); - - AddEventMessage( - "updated language {[Language]}"); - - AddEventMessage( - "changed master language to {[Language]}"); - - AddEventMessage( - "added pattern {[Name]}"); - - AddEventMessage( - "deleted pattern {[PatternId]}"); - - AddEventMessage( - "updated pattern {[Name]}"); - - AddEventMessage( - "added role {[Name]}"); - - AddEventMessage( - "deleted role {[Name]}"); - - AddEventMessage( - "updated role {[Name]}"); - } - - private HistoryEvent CreateEvent(IEvent @event) - { - switch (@event) - { - case AppContributorAssigned e: - return CreateContributorsEvent(e, e.ContributorId, e.Role); - case AppContributorRemoved e: - return CreateContributorsEvent(e, e.ContributorId); - case AppClientAttached e: - return CreateClientsEvent(e, e.Id); - case AppClientRenamed e: - return CreateClientsEvent(e, e.Id, ClientName(e)); - case AppClientRevoked e: - return CreateClientsEvent(e, e.Id); - case AppLanguageAdded e: - return CreateLanguagesEvent(e, e.Language); - case AppLanguageUpdated e: - return CreateLanguagesEvent(e, e.Language); - case AppMasterLanguageSet e: - return CreateLanguagesEvent(e, e.Language); - case AppLanguageRemoved e: - return CreateLanguagesEvent(e, e.Language); - case AppPatternAdded e: - return CreatePatternsEvent(e, e.PatternId, e.Name); - case AppPatternUpdated e: - return CreatePatternsEvent(e, e.PatternId, e.Name); - case AppPatternDeleted e: - return CreatePatternsEvent(e, e.PatternId); - case AppRoleAdded e: - return CreateRolesEvent(e, e.Name); - case AppRoleUpdated e: - return CreateRolesEvent(e, e.Name); - case AppRoleDeleted e: - return CreateRolesEvent(e, e.Name); - case AppPlanChanged e: - return CreatePlansEvent(e, e.PlanId); - case AppPlanReset e: - return CreatePlansEvent(e); - } - - return null; - } - - private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string role = null) - { - return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); - } - - private HistoryEvent CreateLanguagesEvent(IEvent e, Language language) - { - return ForEvent(e, "settings.languages").Param("Language", language); - } - - private HistoryEvent CreateRolesEvent(IEvent e, string name) - { - return ForEvent(e, "settings.roles").Param("Name", name); - } - - private HistoryEvent CreatePatternsEvent(IEvent e, Guid id, string name = null) - { - return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name); - } - - private HistoryEvent CreateClientsEvent(IEvent e, string id, string name = null) - { - return ForEvent(e, "settings.clients").Param("Id", id).Param("Name", name); - } - - private HistoryEvent CreatePlansEvent(IEvent e, string plan = null) - { - return ForEvent(e, "settings.plan").Param("Plan", plan); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - return Task.FromResult(CreateEvent(@event.Payload)); - } - - private static string ClientName(AppClientRenamed e) - { - return !string.IsNullOrWhiteSpace(e.Name) ? e.Name : e.Id; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs deleted file mode 100644 index e9182b39d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppUISettings : IAppUISettings - { - private readonly IGrainFactory grainFactory; - - public AppUISettings(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task GetAsync(Guid appId, string userId) - { - var result = await GetGrain(appId, userId).GetAsync(); - - return result.Value; - } - - public Task RemoveAsync(Guid appId, string userId, string path) - { - return GetGrain(appId, userId).RemoveAsync(path); - } - - public Task SetAsync(Guid appId, string userId, string path, IJsonValue value) - { - return GetGrain(appId, userId).SetAsync(path, value.AsJ()); - } - - public Task SetAsync(Guid appId, string userId, JsonObject settings) - { - return GetGrain(appId, userId).SetAsync(settings.AsJ()); - } - - private IAppUISettingsGrain GetGrain(Guid appId, string userId) - { - return grainFactory.GetGrain(Key(appId, userId)); - } - - private string Key(Guid appId, string userId) - { - if (!string.IsNullOrWhiteSpace(userId)) - { - return $"{appId}_{userId}"; - } - else - { - return $"{appId}"; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs deleted file mode 100644 index 519afc570..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs +++ /dev/null @@ -1,115 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain - { - private readonly IGrainState state; - - [CollectionName("UISettings")] - public sealed class GrainState - { - public JsonObject Settings { get; set; } = JsonValue.Object(); - } - - public AppUISettingsGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task> GetAsync() - { - return Task.FromResult(state.Value.Settings.AsJ()); - } - - public Task SetAsync(J settings) - { - state.Value.Settings = settings; - - return state.WriteAsync(); - } - - public Task SetAsync(string path, J value) - { - var container = GetContainer(path, true, out var key); - - if (container == null) - { - throw new InvalidOperationException("Path does not lead to an object."); - } - - container[key] = value.Value; - - return state.WriteAsync(); - } - - public async Task RemoveAsync(string path) - { - var container = GetContainer(path, false, out var key); - - if (container?.ContainsKey(key) == true) - { - container.Remove(key); - - await state.WriteAsync(); - } - } - - private JsonObject GetContainer(string path, bool add, out string key) - { - Guard.NotNullOrEmpty(path, nameof(path)); - - var segments = path.Split('.'); - - key = segments[segments.Length - 1]; - - var current = state.Value.Settings; - - if (segments.Length > 1) - { - foreach (var segment in segments.Take(segments.Length - 1)) - { - if (!current.TryGetValue(segment, out var temp)) - { - if (add) - { - temp = JsonValue.Object(); - - current[segment] = temp; - } - else - { - return null; - } - } - - if (temp is JsonObject next) - { - current = next; - } - else - { - return null; - } - } - } - - return current; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs deleted file mode 100644 index a137a0c72..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ /dev/null @@ -1,201 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class BackupApps : BackupHandler - { - private const string UsersFile = "Users.json"; - private const string SettingsFile = "Settings.json"; - private readonly IAppUISettings appUISettings; - private readonly IAppsIndex appsIndex; - private readonly IUserResolver userResolver; - private readonly HashSet contributors = new HashSet(); - private readonly Dictionary userMapping = new Dictionary(); - private Dictionary usersWithEmail = new Dictionary(); - private string appReservation; - private string appName; - - public override string Name { get; } = "Apps"; - - public BackupApps(IAppUISettings appUISettings, IAppsIndex appsIndex, IUserResolver userResolver) - { - Guard.NotNull(appsIndex, nameof(appsIndex)); - Guard.NotNull(appUISettings, nameof(appUISettings)); - Guard.NotNull(userResolver, nameof(userResolver)); - - this.appsIndex = appsIndex; - this.appUISettings = appUISettings; - this.userResolver = userResolver; - } - - public override async Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) - { - if (@event.Payload is AppContributorAssigned appContributorAssigned) - { - var userId = appContributorAssigned.ContributorId; - - if (!usersWithEmail.ContainsKey(userId)) - { - var user = await userResolver.FindByIdOrEmailAsync(userId); - - if (user != null) - { - usersWithEmail.Add(userId, user.Email); - } - } - } - } - - public override async Task BackupAsync(Guid appId, BackupWriter writer) - { - await WriteUsersAsync(writer); - await WriteSettingsAsync(writer, appId); - } - - public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case AppCreated appCreated: - { - appName = appCreated.Name; - - await ResolveUsersAsync(reader); - await ReserveAppAsync(appId); - - break; - } - - case AppContributorAssigned contributorAssigned: - { - if (!userMapping.TryGetValue(contributorAssigned.ContributorId, out var user) || user.Equals(actor)) - { - return false; - } - - contributorAssigned.ContributorId = user.Identifier; - contributors.Add(contributorAssigned.ContributorId); - break; - } - - case AppContributorRemoved contributorRemoved: - { - if (!userMapping.TryGetValue(contributorRemoved.ContributorId, out var user) || user.Equals(actor)) - { - return false; - } - - contributorRemoved.ContributorId = user.Identifier; - contributors.Remove(contributorRemoved.ContributorId); - break; - } - } - - if (@event.Payload is SquidexEvent squidexEvent) - { - squidexEvent.Actor = MapUser(squidexEvent.Actor.Identifier, actor); - } - - return true; - } - - public override Task RestoreAsync(Guid appId, BackupReader reader) - { - return ReadSettingsAsync(reader, appId); - } - - private async Task ReserveAppAsync(Guid appId) - { - appReservation = await appsIndex.ReserveAsync(appId, appName); - - if (appReservation == null) - { - throw new BackupRestoreException("The app id or name is not available."); - } - } - - public override async Task CleanupRestoreErrorAsync(Guid appId) - { - await appsIndex.RemoveReservationAsync(appReservation); - } - - private RefToken MapUser(string userId, RefToken fallback) - { - return userMapping.GetOrAdd(userId, fallback); - } - - private async Task ResolveUsersAsync(BackupReader reader) - { - await ReadUsersAsync(reader); - - foreach (var kvp in usersWithEmail) - { - var email = kvp.Value; - - var user = await userResolver.FindByIdOrEmailAsync(email); - - if (user == null && await userResolver.CreateUserIfNotExists(kvp.Value)) - { - user = await userResolver.FindByIdOrEmailAsync(email); - } - - if (user != null) - { - userMapping[kvp.Key] = new RefToken(RefTokenType.Subject, user.Id); - } - } - } - - private async Task ReadUsersAsync(BackupReader reader) - { - var json = await reader.ReadJsonAttachmentAsync>(UsersFile); - - usersWithEmail = json; - } - - private async Task WriteUsersAsync(BackupWriter writer) - { - var json = usersWithEmail; - - await writer.WriteJsonAsync(UsersFile, json); - } - - private async Task WriteSettingsAsync(BackupWriter writer, Guid appId) - { - var json = await appUISettings.GetAsync(appId, null); - - await writer.WriteJsonAsync(SettingsFile, json); - } - - private async Task ReadSettingsAsync(BackupReader reader, Guid appId) - { - var json = await reader.ReadJsonAttachmentAsync(SettingsFile); - - await appUISettings.SetAsync(appId, null, json); - } - - public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) - { - await appsIndex.AddAsync(appReservation); - - await appsIndex.RebuildByContributorsAsync(appId, contributors); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs deleted file mode 100644 index 30873adbb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public sealed class AddPattern : AppCommand - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - - public AddPattern() - { - PatternId = Guid.NewGuid(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs deleted file mode 100644 index 8b5f0ef32..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public sealed class UpdateApp : AppCommand - { - public string Label { get; set; } - - public string Description { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs deleted file mode 100644 index 415856189..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public sealed class UpdatePattern : AppCommand - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs b/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs deleted file mode 100644 index 4977a743e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class DefaultAppLogStore : IAppLogStore - { - private readonly ILogStore logStore; - - public DefaultAppLogStore(ILogStore logStore) - { - Guard.NotNull(logStore, nameof(logStore)); - - this.logStore = logStore; - } - - public Task ReadLogAsync(string appId, DateTime from, DateTime to, Stream stream) - { - Guard.NotNull(appId, nameof(appId)); - - return logStore.ReadLogAsync(appId, from, to, stream); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs deleted file mode 100644 index 729ef1441..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics -{ - public sealed class OrleansAppsHealthCheck : IHealthCheck - { - private readonly IAppsByNameIndexGrain index; - - public OrleansAppsHealthCheck(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - index = grainFactory.GetGrain(SingleGrain.Id); - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - await index.CountAsync(); - - return HealthCheckResult.Healthy("Orleans must establish communication."); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs deleted file mode 100644 index 4360e60f4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardApp - { - public static void CanCreate(CreateApp command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot create app.", e => - { - if (!command.Name.IsSlug()) - { - e(Not.ValidSlug("Name"), nameof(command.Name)); - } - }); - } - - public static void CanUploadImage(UploadAppImage command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot upload image.", e => - { - if (command.File == null) - { - e(Not.Defined("File"), nameof(command.File)); - } - }); - } - - public static void CanUpdate(UpdateApp command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanRemoveImage(RemoveAppImage command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanChangePlan(ChangePlan command, AppPlan plan, IAppPlansProvider appPlans) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot change plan.", e => - { - if (string.IsNullOrWhiteSpace(command.PlanId)) - { - e(Not.Defined("Plan id"), nameof(command.PlanId)); - return; - } - - if (appPlans.GetPlan(command.PlanId) == null) - { - e("A plan with this id does not exist.", nameof(command.PlanId)); - } - - if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) - { - e("Plan can only changed from the user who configured the plan initially."); - } - - if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) - { - e("App has already this plan."); - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs deleted file mode 100644 index 3334518c5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppClients - { - public static void CanAttach(AppClients clients, AttachClient command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot attach client.", e => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Client id"), nameof(command.Id)); - } - else if (clients.ContainsKey(command.Id)) - { - e("A client with the same id already exists."); - } - }); - } - - public static void CanRevoke(AppClients clients, RevokeClient command) - { - Guard.NotNull(command, nameof(command)); - - GetClientOrThrow(clients, command.Id); - - Validate.It(() => "Cannot revoke client.", e => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Client id"), nameof(command.Id)); - } - }); - } - - public static void CanUpdate(AppClients clients, UpdateClient command, Roles roles) - { - Guard.NotNull(command, nameof(command)); - - var client = GetClientOrThrow(clients, command.Id); - - Validate.It(() => "Cannot update client.", e => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Client id"), nameof(command.Id)); - } - - if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null) - { - e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); - } - - if (command.Role != null && !roles.Contains(command.Role)) - { - e(Not.Valid("role"), nameof(command.Role)); - } - - if (client == null) - { - return; - } - - if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) - { - e(Not.New("Client", "name"), nameof(command.Name)); - } - - if (command.Role == client.Role) - { - e(Not.New("Client", "role"), nameof(command.Role)); - } - }); - } - - private static AppClient GetClientOrThrow(AppClients clients, string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - return null; - } - - if (!clients.TryGetValue(id, out var client)) - { - throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity)); - } - - return client; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs deleted file mode 100644 index 120b0d44d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppContributors - { - public static Task CanAssign(AppContributors contributors, Roles roles, AssignContributor command, IUserResolver users, IAppLimitsPlan plan) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot assign contributor.", async e => - { - if (!roles.Contains(command.Role)) - { - e(Not.Valid("role"), nameof(command.Role)); - } - - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - e(Not.Defined("Contributor id"), nameof(command.ContributorId)); - } - else - { - var user = await users.FindByIdOrEmailAsync(command.ContributorId); - - if (user == null) - { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); - } - - command.ContributorId = user.Id; - - if (!command.IsRestore) - { - if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) - { - throw new DomainForbiddenException("You cannot change your own role."); - } - - if (contributors.TryGetValue(command.ContributorId, out var role)) - { - if (role == command.Role) - { - e(Not.New("Contributor", "role"), nameof(command.Role)); - } - } - else - { - if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) - { - e("You have reached the maximum number of contributors for your plan."); - } - } - } - } - }); - } - - public static void CanRemove(AppContributors contributors, RemoveContributor command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot remove contributor.", e => - { - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - e(Not.Defined("Contributor id"), nameof(command.ContributorId)); - } - - var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); - - if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) - { - e("Cannot remove the only owner."); - } - }); - - if (!contributors.ContainsKey(command.ContributorId)) - { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs deleted file mode 100644 index a8a197271..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppLanguages - { - public static void CanAdd(LanguagesConfig languages, AddLanguage command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add language.", e => - { - if (command.Language == null) - { - e(Not.Defined("Language code"), nameof(command.Language)); - } - else if (languages.Contains(command.Language)) - { - e("Language has already been added."); - } - }); - } - - public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) - { - Guard.NotNull(command, nameof(command)); - - var config = GetConfigOrThrow(languages, command.Language); - - Validate.It(() => "Cannot remove language.", e => - { - if (command.Language == null) - { - e(Not.Defined("Language code"), nameof(command.Language)); - } - - if (languages.Master == config) - { - e("Master language cannot be removed."); - } - }); - } - - public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) - { - Guard.NotNull(command, nameof(command)); - - var config = GetConfigOrThrow(languages, command.Language); - - Validate.It(() => "Cannot update language.", e => - { - if (command.Language == null) - { - e(Not.Defined("Language code"), nameof(command.Language)); - } - - if ((languages.Master == config || command.IsMaster) && command.IsOptional) - { - e("Master language cannot be made optional.", nameof(command.IsMaster)); - } - - if (command.Fallback == null) - { - return; - } - - foreach (var fallback in command.Fallback) - { - if (!languages.Contains(fallback)) - { - e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback)); - } - } - }); - } - - private static LanguageConfig GetConfigOrThrow(LanguagesConfig languages, Language language) - { - if (language == null) - { - return null; - } - - if (!languages.TryGetConfig(language, out var languageConfig)) - { - throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity)); - } - - return languageConfig; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs deleted file mode 100644 index d5c0437d9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== -using System; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppPatterns - { - public static void CanAdd(AppPatterns patterns, AddPattern command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add pattern.", e => - { - if (command.PatternId == Guid.Empty) - { - e(Not.Defined("Id"), nameof(command.PatternId)); - } - - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - - if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("A pattern with the same name already exists."); - } - - if (string.IsNullOrWhiteSpace(command.Pattern)) - { - e(Not.Defined("Pattern"), nameof(command.Pattern)); - } - else if (!command.Pattern.IsValidRegex()) - { - e(Not.Valid("Pattern"), nameof(command.Pattern)); - } - - if (patterns.Values.Any(x => x.Pattern == command.Pattern)) - { - e("This pattern already exists but with another name."); - } - }); - } - - public static void CanDelete(AppPatterns patterns, DeletePattern command) - { - Guard.NotNull(command, nameof(command)); - - if (!patterns.ContainsKey(command.PatternId)) - { - throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); - } - } - - public static void CanUpdate(AppPatterns patterns, UpdatePattern command) - { - Guard.NotNull(command, nameof(command)); - - if (!patterns.ContainsKey(command.PatternId)) - { - throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); - } - - Validate.It(() => "Cannot update pattern.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - - if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("A pattern with the same name already exists."); - } - - if (string.IsNullOrWhiteSpace(command.Pattern)) - { - e(Not.Defined("Pattern"), nameof(command.Pattern)); - } - else if (!command.Pattern.IsValidRegex()) - { - e(Not.Valid("Pattern"), nameof(command.Pattern)); - } - - if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern)) - { - e("This pattern already exists but with another name."); - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs deleted file mode 100644 index bd75bc92e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppRoles - { - public static void CanAdd(Roles roles, AddRole command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add role.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - else if (roles.Contains(command.Name)) - { - e("A role with the same name already exists."); - } - }); - } - - public static void CanDelete(Roles roles, DeleteRole command, AppContributors contributors, AppClients clients) - { - Guard.NotNull(command, nameof(command)); - - CheckRoleExists(roles, command.Name); - - Validate.It(() => "Cannot delete role.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - else if (Roles.IsDefault(command.Name)) - { - e("Cannot delete a default role."); - } - - if (clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("Cannot remove a role when a client is assigned."); - } - - if (contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase))) - { - e("Cannot remove a role when a contributor is assigned."); - } - }); - } - - public static void CanUpdate(Roles roles, UpdateRole command) - { - Guard.NotNull(command, nameof(command)); - - CheckRoleExists(roles, command.Name); - - Validate.It(() => "Cannot delete role.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - else if (Roles.IsDefault(command.Name)) - { - e("Cannot update a default role."); - } - - if (command.Permissions == null) - { - e(Not.Defined("Permissions"), nameof(command.Permissions)); - } - }); - } - - private static void CheckRoleExists(Roles roles, string name) - { - if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) - { - return; - } - - if (!roles.ContainsCustom(name)) - { - throw new DomainObjectNotFoundException(name, "Roles", typeof(IAppEntity)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs deleted file mode 100644 index 9ec0f9144..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public static class GuardAppWorkflows - { - public static void CanAdd(AddWorkflow command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add workflow.", e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined("Name"), nameof(command.Name)); - } - }); - } - - public static void CanUpdate(Workflows workflows, UpdateWorkflow command) - { - Guard.NotNull(command, nameof(command)); - - CheckWorkflowExists(workflows, command.WorkflowId); - - Validate.It(() => "Cannot update workflow.", e => - { - if (command.Workflow == null) - { - e(Not.Defined("Workflow"), nameof(command.Workflow)); - return; - } - - var workflow = command.Workflow; - - if (!workflow.Steps.ContainsKey(workflow.Initial)) - { - e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); - } - - if (workflow.Initial == Status.Published) - { - e("Initial step cannot be published step.", $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); - } - - var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; - - if (!workflow.Steps.ContainsKey(Status.Published)) - { - e("Workflow must have a published step.", stepsPrefix); - } - - foreach (var step in workflow.Steps) - { - var stepPrefix = $"{stepsPrefix}.{step.Key}"; - - if (step.Value == null) - { - e(Not.Defined("Step"), stepPrefix); - } - else - { - foreach (var transition in step.Value.Transitions) - { - var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}"; - - if (!workflow.Steps.ContainsKey(transition.Key)) - { - e("Transition has an invalid target.", transitionPrefix); - } - - if (transition.Value == null) - { - e(Not.Defined("Transition"), transitionPrefix); - } - } - } - } - }); - } - - public static void CanDelete(Workflows workflows, DeleteWorkflow command) - { - Guard.NotNull(command, nameof(command)); - - CheckWorkflowExists(workflows, command.WorkflowId); - } - - private static void CheckWorkflowExists(Workflows workflows, Guid id) - { - if (!workflows.ContainsKey(id)) - { - throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs deleted file mode 100644 index 4b959e6e4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public interface IAppEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - string Name { get; } - - string Label { get; } - - string Description { get; } - - Roles Roles { get; } - - AppPlan Plan { get; } - - AppImage Image { get; } - - AppClients Clients { get; } - - AppPatterns Patterns { get; } - - AppContributors Contributors { get; } - - LanguagesConfig LanguagesConfig { get; } - - Workflows Workflows { get; } - - bool IsArchived { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs deleted file mode 100644 index d9e0f8d45..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public interface IAppUISettings - { - Task GetAsync(Guid appId, string userId); - - Task SetAsync(Guid appId, string userId, string path, IJsonValue value); - - Task SetAsync(Guid appId, string userId, JsonObject settings); - - Task RemoveAsync(Guid appId, string userId, string path); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs deleted file mode 100644 index 025f33ee6..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsIndex : IAppsIndex, ICommandMiddleware - { - private readonly IGrainFactory grainFactory; - - public AppsIndex(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task RebuildByContributorsAsync(Guid appId, HashSet contributors) - { - foreach (var contributorId in contributors) - { - await Index(contributorId).AddAsync(appId); - } - } - - public Task RebuildByContributorsAsync(string contributorId, HashSet apps) - { - return Index(contributorId).RebuildAsync(apps); - } - - public Task RebuildAsync(Dictionary appsByName) - { - return Index().RebuildAsync(appsByName); - } - - public Task RemoveReservationAsync(string token) - { - return Index().RemoveReservationAsync(token); - } - - public Task> GetIdsAsync() - { - return Index().GetIdsAsync(); - } - - public Task AddAsync(string token) - { - return Index().AddAsync(token); - } - - public Task ReserveAsync(Guid id, string name) - { - return Index().ReserveAsync(id, name); - } - - public async Task> GetAppsAsync() - { - using (Profiler.TraceMethod()) - { - var ids = await GetAppIdsAsync(); - - var apps = - await Task.WhenAll(ids - .Select(id => GetAppAsync(id))); - - return apps.Where(x => x != null).ToList(); - } - } - - public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions) - { - using (Profiler.TraceMethod()) - { - var ids = - await Task.WhenAll( - GetAppIdsByUserAsync(userId), - GetAppIdsAsync(permissions.ToAppNames())); - - var apps = - await Task.WhenAll(ids - .SelectMany(x => x) - .Select(id => GetAppAsync(id))); - - return apps.Where(x => x != null).ToList(); - } - } - - public async Task GetAppByNameAsync(string name) - { - using (Profiler.TraceMethod()) - { - var appId = await GetAppIdAsync(name); - - if (appId == default) - { - return null; - } - - return await GetAppAsync(appId); - } - } - - public async Task GetAppAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (IsFound(app.Value)) - { - return app.Value; - } - - return null; - } - } - - private async Task> GetAppIdsByUserAsync(string userId) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(userId).GetIdsAsync(); - } - } - - private async Task> GetAppIdsAsync() - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(); - } - } - - private async Task> GetAppIdsAsync(string[] names) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(names); - } - } - - private async Task GetAppIdAsync(string name) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdAsync(name); - } - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is CreateApp createApp) - { - var index = Index(); - - string token = await CheckAppAsync(index, createApp); - - try - { - await next(); - } - finally - { - if (token != null) - { - if (context.IsCompleted) - { - await index.AddAsync(token); - - if (createApp.Actor.IsSubject) - { - await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId); - } - } - else - { - await index.RemoveReservationAsync(token); - } - } - } - } - else - { - await next(); - - if (context.IsCompleted) - { - switch (context.Command) - { - case AssignContributor assignContributor: - await AssignContributorAsync(assignContributor); - break; - - case RemoveContributor removeContributor: - await RemoveContributorAsync(removeContributor); - break; - - case ArchiveApp archiveApp: - await ArchiveAppAsync(archiveApp); - break; - } - } - } - } - - private async Task CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command) - { - var name = command.Name; - - if (name.IsSlug()) - { - var token = await index.ReserveAsync(command.AppId, name); - - if (token == null) - { - var error = new ValidationError("An app with this already exists."); - - throw new ValidationException("Cannot create app.", error); - } - - return token; - } - - return null; - } - - private Task AssignContributorAsync(AssignContributor command) - { - return Index(command.ContributorId).AddAsync(command.AppId); - } - - private Task RemoveContributorAsync(RemoveContributor command) - { - return Index(command.ContributorId).RemoveAsync(command.AppId); - } - - private async Task ArchiveAppAsync(ArchiveApp command) - { - var appId = command.AppId; - - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (IsFound(app.Value)) - { - await Index().RemoveAsync(appId); - } - - foreach (var contributorId in app.Value.Contributors.Keys) - { - await Index(contributorId).RemoveAsync(appId); - } - } - - private static bool IsFound(IAppEntity app) - { - return app.Version > EtagVersion.Empty && !app.IsArchived; - } - - private IAppsByNameIndexGrain Index() - { - return grainFactory.GetGrain(SingleGrain.Id); - } - - private IAppsByUserIndexGrain Index(string id) - { - return grainFactory.GetGrain(id); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs deleted file mode 100644 index 17b9f9aeb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public interface IAppsIndex - { - Task> GetIdsAsync(); - - Task> GetAppsAsync(); - - Task> GetAppsForUserAsync(string userId, PermissionSet permissions); - - Task GetAppByNameAsync(string name); - - Task GetAppAsync(Guid appId); - - Task ReserveAsync(Guid id, string name); - - Task AddAsync(string token); - - Task RemoveReservationAsync(string token); - - Task RebuildByContributorsAsync(string contributorId, HashSet apps); - - Task RebuildAsync(Dictionary apps); - - Task RebuildByContributorsAsync(Guid appId, HashSet contributors); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs deleted file mode 100644 index 360335f10..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps.Invitation -{ - public sealed class InviteUserCommandMiddleware : ICommandMiddleware - { - private readonly IUserResolver userResolver; - - public InviteUserCommandMiddleware(IUserResolver userResolver) - { - Guard.NotNull(userResolver, nameof(userResolver)); - - this.userResolver = userResolver; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is AssignContributor assignContributor && ShouldInvite(assignContributor)) - { - var created = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId, true); - - await next(); - - if (created && context.PlainResult is IAppEntity app) - { - context.Complete(new InvitedResult { App = app }); - } - } - else - { - await next(); - } - } - - private static bool ShouldInvite(AssignContributor assignContributor) - { - return assignContributor.Invite && assignContributor.ContributorId.IsEmail(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs deleted file mode 100644 index a5db89f6f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -#pragma warning disable IDE0028 // Simplify collection initialization - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class RolePermissionsProvider - { - private readonly IAppProvider appProvider; - - public RolePermissionsProvider(IAppProvider appProvider) - { - Guard.NotNull(appProvider, nameof(appProvider)); - - this.appProvider = appProvider; - } - - public async Task> GetPermissionsAsync(IAppEntity app) - { - var schemaNames = await GetSchemaNamesAsync(app); - - var result = new List { Permission.Any }; - - foreach (var permission in Permissions.ForAppsNonSchema) - { - if (permission.Length > Permissions.App.Length + 1) - { - var trimmed = permission.Substring(Permissions.App.Length + 1); - - if (trimmed.Length > 0) - { - result.Add(trimmed); - } - } - } - - foreach (var permission in Permissions.ForAppsSchema) - { - var trimmed = permission.Substring(Permissions.App.Length + 1); - - foreach (var schema in schemaNames) - { - var replaced = trimmed.Replace("{name}", schema); - - result.Add(replaced); - } - } - - return result; - } - - private async Task> GetSchemaNamesAsync(IAppEntity app) - { - var schemas = await appProvider.GetSchemasAsync(app.Id); - - var schemaNames = new List(); - - schemaNames.Add(Permission.Any); - schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name)); - - return schemaNames; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs deleted file mode 100644 index 59d0feed4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public interface IAppLimitsPlan - { - string Id { get; } - - string Name { get; } - - string Costs { get; } - - string YearlyCosts { get; } - - string YearlyId { get; } - - long MaxApiCalls { get; } - - long MaxAssetSize { get; } - - int MaxContributors { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs deleted file mode 100644 index 933a11ddf..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public interface IAppPlanBillingManager - { - bool HasPortal { get; } - - Task ChangePlanAsync(string userId, NamedId appId, string planId); - - Task GetPortalLinkAsync(string userId); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs deleted file mode 100644 index cd4fb8b03..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// 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.Entities.Apps.Services -{ - public interface IAppPlansProvider - { - IEnumerable GetAvailablePlans(); - - bool IsConfiguredPlan(string planId); - - IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app); - - IAppLimitsPlan GetPlanUpgrade(string planId); - - IAppLimitsPlan GetPlanForApp(IAppEntity app); - - IAppLimitsPlan GetPlan(string planId); - - IAppLimitsPlan GetFreePlan(); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs deleted file mode 100644 index 3d568c928..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations -{ - public sealed class ConfigAppLimitsPlan : IAppLimitsPlan - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Costs { get; set; } - - public string YearlyCosts { get; set; } - - public string YearlyId { get; set; } - - public long MaxApiCalls { get; set; } - - public long MaxAssetSize { get; set; } - - public int MaxContributors { get; set; } - - public ConfigAppLimitsPlan Clone() - { - return (ConfigAppLimitsPlan)MemberwiseClone(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs deleted file mode 100644 index 3da2ae205..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// 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.Entities.Apps.Services.Implementations -{ - public sealed class ConfigAppPlansProvider : IAppPlansProvider - { - private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1 - }; - - private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly List plansList = new List(); - - public ConfigAppPlansProvider(IEnumerable config) - { - Guard.NotNull(config, nameof(config)); - - foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone())) - { - plansList.Add(plan); - plansById[plan.Id] = plan; - - if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) - { - plansById[plan.YearlyId] = plan; - } - } - } - - public IEnumerable GetAvailablePlans() - { - return plansList; - } - - public bool IsConfiguredPlan(string planId) - { - return planId != null && plansById.ContainsKey(planId); - } - - public IAppLimitsPlan GetPlanForApp(IAppEntity app) - { - Guard.NotNull(app, nameof(app)); - - return GetPlan(app.Plan?.PlanId); - } - - public IAppLimitsPlan GetPlan(string planId) - { - return GetPlanCore(planId); - } - - public IAppLimitsPlan GetFreePlan() - { - return GetPlanCore(plansList.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.Costs))?.Id); - } - - public IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app) - { - Guard.NotNull(app, nameof(app)); - - return GetPlanUpgrade(app.Plan?.PlanId); - } - - public IAppLimitsPlan GetPlanUpgrade(string planId) - { - var plan = GetPlanCore(planId); - - var nextPlanIndex = plansList.IndexOf(plan); - - if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) - { - return plansList[nextPlanIndex + 1]; - } - - return null; - } - - private ConfigAppLimitsPlan GetPlanCore(string planId) - { - return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs deleted file mode 100644 index b8c1f46ef..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations -{ - public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager - { - public bool HasPortal - { - get { return false; } - } - - public Task ChangePlanAsync(string userId, NamedId appId, string planId) - { - return Task.FromResult(new PlanResetResult()); - } - - public Task GetPortalLinkAsync(string userId) - { - return Task.FromResult(string.Empty); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs deleted file mode 100644 index 9b41c88e2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Services -{ - public sealed class RedirectToCheckoutResult : IChangePlanResult - { - public Uri Url { get; } - - public RedirectToCheckoutResult(Uri url) - { - Guard.NotNull(url, nameof(url)); - - Url = url; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs deleted file mode 100644 index 5247bb3dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ /dev/null @@ -1,252 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Runtime.Serialization; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -#pragma warning disable IDE0060 // Remove unused parameter - -namespace Squidex.Domain.Apps.Entities.Apps.State -{ - [CollectionName("Apps")] - public class AppState : DomainObjectState, IAppEntity - { - [DataMember] - public string Name { get; set; } - - [DataMember] - public string Label { get; set; } - - [DataMember] - public string Description { get; set; } - - [DataMember] - public Roles Roles { get; set; } = Roles.Empty; - - [DataMember] - public AppPlan Plan { get; set; } - - [DataMember] - public AppImage Image { get; set; } - - [DataMember] - public AppClients Clients { get; set; } = AppClients.Empty; - - [DataMember] - public AppPatterns Patterns { get; set; } = AppPatterns.Empty; - - [DataMember] - public AppContributors Contributors { get; set; } = AppContributors.Empty; - - [DataMember] - public LanguagesConfig LanguagesConfig { get; set; } = LanguagesConfig.English; - - [DataMember] - public Workflows Workflows { get; set; } = Workflows.Empty; - - [DataMember] - public bool IsArchived { get; set; } - - public void ApplyEvent(IEvent @event) - { - switch (@event) - { - case AppCreated e: - { - SimpleMapper.Map(e, this); - - break; - } - - case AppUpdated e: - { - SimpleMapper.Map(e, this); - - break; - } - - case AppImageUploaded e: - { - Image = e.Image; - - break; - } - - case AppImageRemoved _: - { - Image = null; - - break; - } - - case AppPlanChanged e: - { - Plan = AppPlan.Build(e.Actor, e.PlanId); - - break; - } - - case AppPlanReset _: - { - Plan = null; - - break; - } - - case AppContributorAssigned e: - { - Contributors = Contributors.Assign(e.ContributorId, e.Role); - - break; - } - - case AppContributorRemoved e: - { - Contributors = Contributors.Remove(e.ContributorId); - - break; - } - - case AppClientAttached e: - { - Clients = Clients.Add(e.Id, e.Secret); - - break; - } - - case AppClientUpdated e: - { - Clients = Clients.Update(e.Id, e.Role); - - break; - } - - case AppClientRenamed e: - { - Clients = Clients.Rename(e.Id, e.Name); - - break; - } - - case AppClientRevoked e: - { - Clients = Clients.Revoke(e.Id); - - break; - } - - case AppWorkflowAdded e: - { - Workflows = Workflows.Add(e.WorkflowId, e.Name); - - break; - } - - case AppWorkflowUpdated e: - { - Workflows = Workflows.Update(e.WorkflowId, e.Workflow); - - break; - } - - case AppWorkflowDeleted e: - { - Workflows = Workflows.Remove(e.WorkflowId); - - break; - } - - case AppPatternAdded e: - { - Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message); - - break; - } - - case AppPatternDeleted e: - { - Patterns = Patterns.Remove(e.PatternId); - - break; - } - - case AppPatternUpdated e: - { - Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message); - - break; - } - - case AppRoleAdded e: - { - Roles = Roles.Add(e.Name); - - break; - } - - case AppRoleDeleted e: - { - Roles = Roles.Remove(e.Name); - - break; - } - - case AppRoleUpdated e: - { - Roles = Roles.Update(e.Name, e.Permissions); - - break; - } - - case AppLanguageAdded e: - { - LanguagesConfig = LanguagesConfig.Set(e.Language); - - break; - } - - case AppLanguageRemoved e: - { - LanguagesConfig = LanguagesConfig.Remove(e.Language); - - break; - } - - case AppLanguageUpdated e: - { - LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback); - - if (e.IsMaster) - { - LanguagesConfig = LanguagesConfig.MakeMaster(e.Language); - } - - break; - } - - case AppArchived _: - { - Plan = null; - - IsArchived = true; - - break; - } - } - } - - public override AppState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs deleted file mode 100644 index 29540e8a5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; - -namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders -{ - public abstract class FieldBuilder - { - private readonly UpsertSchemaField field; - - protected T Properties() where T : FieldProperties - { - return field.Properties as T; - } - - protected FieldBuilder(UpsertSchemaField field) - { - this.field = field; - } - - public FieldBuilder Label(string label) - { - field.Properties.Label = label; - - return this; - } - - public FieldBuilder Hints(string hints) - { - field.Properties.Hints = hints; - - return this; - } - - public FieldBuilder Localizable() - { - field.Partitioning = Partitioning.Language.Key; - - return this; - } - - public FieldBuilder Disabled() - { - field.IsDisabled = true; - - return this; - } - - public FieldBuilder Required() - { - field.Properties.IsRequired = true; - - return this; - } - - public FieldBuilder ShowInList() - { - field.Properties.IsListField = true; - - return this; - } - - public FieldBuilder ShowInReferences() - { - field.Properties.IsReferenceField = true; - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs deleted file mode 100644 index bc588ebb5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders -{ - public sealed class SchemaBuilder - { - private readonly CreateSchema command; - - public SchemaBuilder(CreateSchema command) - { - this.command = command; - } - - public static SchemaBuilder Create(string name) - { - var schemaName = name.ToKebabCase(); - - return new SchemaBuilder(new CreateSchema - { - Name = schemaName - }).Published().WithLabel(name); - } - - public SchemaBuilder WithLabel(string label) - { - command.Properties = command.Properties ?? new SchemaProperties(); - command.Properties.Label = label; - - return this; - } - - public SchemaBuilder WithScripts(SchemaScripts scripts) - { - command.Scripts = scripts; - - return this; - } - - public SchemaBuilder Published() - { - command.IsPublished = true; - - return this; - } - - public SchemaBuilder Singleton() - { - command.IsSingleton = true; - - return this; - } - - public SchemaBuilder AddAssets(string name, Action configure) - { - var field = AddField(name); - - configure(new AssetFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddBoolean(string name, Action configure) - { - var field = AddField(name); - - configure(new BooleanFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddDateTime(string name, Action configure) - { - var field = AddField(name); - - configure(new DateTimeFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddJson(string name, Action configure) - { - var field = AddField(name); - - configure(new JsonFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddNumber(string name, Action configure) - { - var field = AddField(name); - - configure(new NumberFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddString(string name, Action configure) - { - var field = AddField(name); - - configure(new StringFieldBuilder(field)); - - return this; - } - - public SchemaBuilder AddTags(string name, Action configure) - { - var field = AddField(name); - - configure(new TagsFieldBuilder(field)); - - return this; - } - - private UpsertSchemaField AddField(string name) where T : FieldProperties, new() - { - var field = new UpsertSchemaField - { - Name = name.ToCamelCase(), - Properties = new T - { - Label = name - } - }; - - command.Fields = command.Fields ?? new List(); - command.Fields.Add(field); - - return field; - } - - public CreateSchema Build() - { - return command; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs deleted file mode 100644 index 8cd60657d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure.Collections; - -namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders -{ - public class StringFieldBuilder : FieldBuilder - { - public StringFieldBuilder(UpsertSchemaField field) - : base(field) - { - } - - public StringFieldBuilder AsTextArea() - { - Properties().Editor = StringFieldEditor.TextArea; - - return this; - } - - public StringFieldBuilder AsRichText() - { - Properties().Editor = StringFieldEditor.RichText; - - return this; - } - - public StringFieldBuilder AsDropDown(params string[] values) - { - Properties().AllowedValues = ReadOnlyCollection.Create(values); - Properties().Editor = StringFieldEditor.Dropdown; - - return this; - } - - public StringFieldBuilder Pattern(string pattern, string message = null) - { - Properties().Pattern = pattern; - Properties().PatternMessage = message; - - return this; - } - - public StringFieldBuilder Length(int maxLength, int minLength = 0) - { - Properties().MaxLength = maxLength; - Properties().MinLength = minLength; - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs deleted file mode 100644 index fd8fdb53e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class AssetChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - private readonly IAssetLoader assetLoader; - - public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IAssetLoader assetLoader) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(assetLoader, nameof(assetLoader)); - - this.scriptEngine = scriptEngine; - - this.assetLoader = assetLoader; - } - - protected override async Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedAssetEvent(); - - var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber()); - - SimpleMapper.Map(asset, result); - - switch (@event.Payload) - { - case AssetCreated _: - result.Type = EnrichedAssetEventType.Created; - break; - case AssetAnnotated _: - result.Type = EnrichedAssetEventType.Annotated; - break; - case AssetUpdated _: - result.Type = EnrichedAssetEventType.Updated; - break; - case AssetDeleted _: - result.Type = EnrichedAssetEventType.Deleted; - break; - } - - result.Name = $"Asset{result.Type}"; - - return result; - } - - protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) - { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs deleted file mode 100644 index 9b4b3486b..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ /dev/null @@ -1,175 +0,0 @@ -// ========================================================================== -// 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.Security.Cryptography; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class AssetCommandMiddleware : GrainCommandMiddleware - { - private readonly IAssetStore assetStore; - private readonly IAssetEnricher assetEnricher; - private readonly IAssetQueryService assetQuery; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IContextProvider contextProvider; - private readonly IEnumerable> tagGenerators; - - public AssetCommandMiddleware( - IGrainFactory grainFactory, - IAssetEnricher assetEnricher, - IAssetQueryService assetQuery, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IContextProvider contextProvider, - IEnumerable> tagGenerators) - : base(grainFactory) - { - Guard.NotNull(assetEnricher, nameof(assetEnricher)); - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); - Guard.NotNull(contextProvider, nameof(contextProvider)); - Guard.NotNull(tagGenerators, nameof(tagGenerators)); - - this.assetStore = assetStore; - this.assetEnricher = assetEnricher; - this.assetQuery = assetQuery; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.contextProvider = contextProvider; - this.tagGenerators = tagGenerators; - } - - public override async Task HandleAsync(CommandContext context, Func next) - { - var tempFile = context.ContextId.ToString(); - - switch (context.Command) - { - case CreateAsset createAsset: - { - await EnrichWithImageInfosAsync(createAsset); - await EnrichWithHashAndUploadAsync(createAsset, tempFile); - - try - { - var ctx = contextProvider.Context.Clone().WithNoAssetEnrichment(); - - var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); - - foreach (var existing in existings) - { - if (IsDuplicate(existing, createAsset.File)) - { - var result = new AssetCreatedResult(existing, true); - - context.Complete(result); - - await next(); - return; - } - } - - GenerateTags(createAsset); - - await HandleCoreAsync(context, next); - - var asset = context.Result(); - - context.Complete(new AssetCreatedResult(asset, false)); - - await assetStore.CopyAsync(tempFile, createAsset.AssetId.ToString(), asset.FileVersion, null); - } - finally - { - await assetStore.DeleteAsync(tempFile); - } - - break; - } - - case UpdateAsset updateAsset: - { - await EnrichWithImageInfosAsync(updateAsset); - await EnrichWithHashAndUploadAsync(updateAsset, tempFile); - - try - { - await HandleCoreAsync(context, next); - - var asset = context.Result(); - - await assetStore.CopyAsync(tempFile, updateAsset.AssetId.ToString(), asset.FileVersion, null); - } - finally - { - await assetStore.DeleteAsync(tempFile); - } - - break; - } - - default: - await HandleCoreAsync(context, next); - break; - } - } - - private async Task HandleCoreAsync(CommandContext context, Func next) - { - await base.HandleAsync(context, next); - - if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity)) - { - var enriched = await assetEnricher.EnrichAsync(asset, contextProvider.Context); - - context.Complete(enriched); - } - } - - private static bool IsDuplicate(IAssetEntity asset, AssetFile file) - { - return asset?.FileName == file.FileName && asset.FileSize == file.FileSize; - } - - private async Task EnrichWithImageInfosAsync(UploadAssetCommand command) - { - command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - } - - private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) - { - using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256)) - { - await assetStore.UploadAsync(tempFile, hashStream); - - command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64(); - } - } - - private void GenerateTags(CreateAsset createAsset) - { - if (createAsset.Tags == null) - { - createAsset.Tags = new HashSet(); - } - - foreach (var tagGenerator in tagGenerators) - { - tagGenerator.GenerateTags(createAsset, createAsset.Tags); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs deleted file mode 100644 index 23c80b139..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ /dev/null @@ -1,183 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Assets.Guards; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class AssetGrain : LogSnapshotDomainObjectGrain, IAssetGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - private readonly ITagService tagService; - - public AssetGrain(IStore store, ITagService tagService, IActivationLimit limit, ISemanticLog log) - : base(store, log) - { - Guard.NotNull(tagService, nameof(tagService)); - - this.tagService = tagService; - - limit?.SetLimit(5000, Lifetime); - } - - protected override Task OnActivateAsync(Guid key) - { - TryDelayDeactivation(Lifetime); - - return base.OnActivateAsync(key); - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case CreateAsset createAsset: - return CreateReturnAsync(createAsset, async c => - { - GuardAsset.CanCreate(c); - - var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); - - Create(c, tagIds); - - return Snapshot; - }); - case UpdateAsset updateAsset: - return UpdateReturn(updateAsset, c => - { - GuardAsset.CanUpdate(c); - - Update(c); - - return Snapshot; - }); - case AnnotateAsset annotateAsset: - return UpdateReturnAsync(annotateAsset, async c => - { - GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); - - var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); - - Annotate(c, tagIds); - - return Snapshot; - }); - case DeleteAsset deleteAsset: - return UpdateAsync(deleteAsset, async c => - { - GuardAsset.CanDelete(c); - - await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags); - - Delete(c); - }); - default: - throw new NotSupportedException(); - } - } - - private async Task> NormalizeTagsAsync(Guid appId, HashSet tags) - { - if (tags == null) - { - return null; - } - - var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); - - return new HashSet(normalized.Values); - } - - public void Create(CreateAsset command, HashSet tagIds) - { - var @event = SimpleMapper.Map(command, new AssetCreated - { - IsImage = command.ImageInfo != null, - FileName = command.File.FileName, - FileSize = command.File.FileSize, - FileVersion = 0, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - Slug = command.File.FileName.ToAssetSlug() - }); - - @event.Tags = tagIds; - - RaiseEvent(@event); - } - - public void Update(UpdateAsset command) - { - var @event = SimpleMapper.Map(command, new AssetUpdated - { - FileVersion = Snapshot.FileVersion + 1, - FileSize = command.File.FileSize, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - IsImage = command.ImageInfo != null - }); - - RaiseEvent(@event); - } - - public void Annotate(AnnotateAsset command, HashSet tagIds) - { - var @event = SimpleMapper.Map(command, new AssetAnnotated()); - - @event.Tags = tagIds; - - RaiseEvent(@event); - } - - public void Delete(DeleteAsset command) - { - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Asset has already been deleted"); - } - } - - public Task> GetStateAsync(long version = EtagVersion.Any) - { - return J.AsTask(GetSnapshot(version)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs deleted file mode 100644 index eb6eb6168..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.UsageTracking; - -#pragma warning disable CS0649 - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public partial class AssetUsageTracker : IAssetUsageTracker, IEventConsumer - { - private const string Category = "Default"; - private const string CounterTotalCount = "TotalAssets"; - private const string CounterTotalSize = "TotalSize"; - private static readonly DateTime SummaryDate; - private readonly IUsageRepository usageStore; - - public AssetUsageTracker(IUsageRepository usageStore) - { - Guard.NotNull(usageStore, nameof(usageStore)); - - this.usageStore = usageStore; - } - - public async Task GetTotalSizeAsync(Guid appId) - { - var key = GetKey(appId); - - var entries = await usageStore.QueryAsync(key, SummaryDate, SummaryDate); - - return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault(); - } - - public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) - { - var enriched = new List(); - - var usagesFlat = await usageStore.QueryAsync(GetKey(appId), fromDate, toDate); - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - var stored = usagesFlat.FirstOrDefault(x => x.Date == date && x.Category == Category); - - var totalCount = 0L; - var totalSize = 0L; - - if (stored != null) - { - totalCount = (long)stored.Counters.Get(CounterTotalCount); - totalSize = (long)stored.Counters.Get(CounterTotalSize); - } - - enriched.Add(new AssetStats(date, totalCount, totalSize)); - } - - return enriched; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs deleted file mode 100644 index 44701ee16..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed class BackupAssets : BackupHandlerWithStore - { - private const string TagsFile = "AssetTags.json"; - private readonly HashSet assetIds = new HashSet(); - private readonly IAssetStore assetStore; - private readonly ITagService tagService; - - public override string Name { get; } = "Assets"; - - public BackupAssets(IStore store, IAssetStore assetStore, ITagService tagService) - : base(store) - { - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(tagService, nameof(tagService)); - - this.assetStore = assetStore; - - this.tagService = tagService; - } - - public override Task BackupAsync(Guid appId, BackupWriter writer) - { - return BackupTagsAsync(appId, writer); - } - - public override Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) - { - switch (@event.Payload) - { - case AssetCreated assetCreated: - return WriteAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, writer); - case AssetUpdated assetUpdated: - return WriteAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, writer); - } - - return TaskHelper.Done; - } - - public override async Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case AssetCreated assetCreated: - await ReadAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, reader); - break; - case AssetUpdated assetUpdated: - await ReadAssetAsync(assetUpdated.AssetId, assetUpdated.FileVersion, reader); - break; - } - - return true; - } - - public override async Task RestoreAsync(Guid appId, BackupReader reader) - { - await RestoreTagsAsync(appId, reader); - - await RebuildManyAsync(assetIds, RebuildAsync); - } - - private async Task RestoreTagsAsync(Guid appId, BackupReader reader) - { - var tags = await reader.ReadJsonAttachmentAsync(TagsFile); - - await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); - } - - private async Task BackupTagsAsync(Guid appId, BackupWriter writer) - { - var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); - - await writer.WriteJsonAsync(TagsFile, tags); - } - - private Task WriteAssetAsync(Guid assetId, long fileVersion, BackupWriter writer) - { - return writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => - { - return assetStore.DownloadAsync(assetId.ToString(), fileVersion, null, stream); - }); - } - - private Task ReadAssetAsync(Guid assetId, long fileVersion, BackupReader reader) - { - assetIds.Add(assetId); - - return reader.ReadBlobAsync(GetName(reader.OldGuid(assetId), fileVersion), async stream => - { - try - { - await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream, true); - } - catch (AssetAlreadyExistsException) - { - return; - } - }); - } - - private static string GetName(Guid assetId, long fileVersion) - { - return $"{assetId}_{fileVersion}.asset"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs deleted file mode 100644 index 5ef0652cd..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Assets; - -namespace Squidex.Domain.Apps.Entities.Assets.Commands -{ - public abstract class UploadAssetCommand : AssetCommand - { - public AssetFile File { get; set; } - - public ImageInfo ImageInfo { get; set; } - - public string FileHash { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs deleted file mode 100644 index 06746132f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Assets.Guards -{ - public static class GuardAsset - { - public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot rename asset.", e => - { - if (string.IsNullOrWhiteSpace(command.FileName) && - string.IsNullOrWhiteSpace(command.Slug) && - command.Tags == null) - { - e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags)); - } - - if (!string.IsNullOrWhiteSpace(command.FileName) && string.Equals(command.FileName, oldFileName)) - { - e(Not.New("Asset", "name"), nameof(command.FileName)); - } - - if (!string.IsNullOrWhiteSpace(command.Slug) && string.Equals(command.Slug, oldSlug)) - { - e(Not.New("Asset", "slug"), nameof(command.Slug)); - } - }); - } - - public static void CanCreate(CreateAsset command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanUpdate(UpdateAsset command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanDelete(DeleteAsset command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs deleted file mode 100644 index 395a4d86f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public interface IAssetQueryService - { - Task> QueryByHashAsync(Context context, Guid appId, string hash); - - Task> QueryAsync(Context context, Q query); - - Task FindAssetAsync(Context context, Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs deleted file mode 100644 index 3c79c93d5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class AssetEnricher : IAssetEnricher - { - private readonly ITagService tagService; - - public AssetEnricher(ITagService tagService) - { - Guard.NotNull(tagService, nameof(tagService)); - - this.tagService = tagService; - } - - public async Task EnrichAsync(IAssetEntity asset, Context context) - { - Guard.NotNull(asset, nameof(asset)); - Guard.NotNull(context, nameof(context)); - - var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1), context); - - return enriched[0]; - } - - public async Task> EnrichAsync(IEnumerable assets, Context context) - { - Guard.NotNull(assets, nameof(assets)); - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); - - if (ShouldEnrich(context)) - { - await EnrichTagsAsync(results); - } - - return results; - } - } - - private async Task EnrichTagsAsync(List assets) - { - foreach (var group in assets.GroupBy(x => x.AppId.Id)) - { - var tagsById = await CalculateTags(group); - - foreach (var asset in group) - { - asset.TagNames = new HashSet(); - - if (asset.Tags != null) - { - foreach (var id in asset.Tags) - { - if (tagsById.TryGetValue(id, out var name)) - { - asset.TagNames.Add(name); - } - } - } - } - } - } - - private async Task> CalculateTags(IGrouping group) - { - var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); - - return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); - } - - private static bool ShouldEnrich(Context context) - { - return !context.IsNoAssetEnrichment(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs deleted file mode 100644 index 82adbe7db..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class AssetLoader : IAssetLoader - { - private readonly IGrainFactory grainFactory; - - public AssetLoader(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task GetAsync(Guid id, long version) - { - using (Profiler.TraceMethod()) - { - var grain = grainFactory.GetGrain(id); - - var content = await grain.GetStateAsync(version); - - if (content.Value == null || content.Value.Version != version) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IAssetEntity)); - } - - return content.Value; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs deleted file mode 100644 index 66efc0a53..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ /dev/null @@ -1,174 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using Microsoft.OData; -using Microsoft.OData.Edm; -using NJsonSchema; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Queries.OData; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public class AssetQueryParser - { - private readonly JsonSchema jsonSchema = BuildJsonSchema(); - private readonly IEdmModel edmModel = BuildEdmModel(); - private readonly IJsonSerializer jsonSerializer; - private readonly ITagService tagService; - private readonly AssetOptions options; - - public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions options) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - Guard.NotNull(options, nameof(options)); - Guard.NotNull(tagService, nameof(tagService)); - - this.jsonSerializer = jsonSerializer; - this.options = options.Value; - this.tagService = tagService; - } - - public virtual ClrQuery ParseQuery(Context context, Q q) - { - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var result = new ClrQuery(); - - if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) - { - result = ParseJson(q.JsonQuery); - } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) - { - result = ParseOData(q.ODataQuery); - } - - if (result.Filter != null) - { - result.Filter = FilterTagTransformer.Transform(result.Filter, context.App.Id, tagService); - } - - if (result.Sort.Count == 0) - { - result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); - } - - if (result.Take == long.MaxValue) - { - result.Take = options.DefaultPageSize; - } - else if (result.Take > options.MaxResults) - { - result.Take = options.MaxResults; - } - - return result; - } - } - - private ClrQuery ParseJson(string json) - { - return jsonSchema.Parse(json, jsonSerializer); - } - - private ClrQuery ParseOData(string odata) - { - try - { - return edmModel.ParseQuery(odata).ToQuery(); - } - catch (NotSupportedException) - { - throw new ValidationException("OData operation is not supported."); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - } - - private static JsonSchema BuildJsonSchema() - { - var schema = new JsonSchema { Title = "Asset", Type = JsonObjectType.Object }; - - void AddProperty(string name, JsonObjectType type, string format = null) - { - var property = new JsonSchemaProperty { Type = type, Format = format }; - - schema.Properties[name.ToCamelCase()] = property; - } - - AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid); - AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime); - AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime); - AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean); - AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer); - AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); - AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); - - return schema; - } - - private static IEdmModel BuildEdmModel() - { - var entityType = new EdmEntityType("Squidex", "Asset"); - - void AddProperty(string name, EdmPrimitiveTypeKind type) - { - entityType.AddStructuralProperty(name.ToCamelCase(), type); - } - - AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); - AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); - AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64); - AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); - AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); - AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); - AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); - AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); - AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String); - AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String); - - var container = new EdmEntityContainer("Squidex", "Container"); - - container.AddEntitySet("AssetSet", entityType); - - var model = new EdmModel(); - - model.AddElement(container); - model.AddElement(entityType); - - return model; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs deleted file mode 100644 index 376a057c1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class AssetQueryService : IAssetQueryService - { - private readonly IAssetEnricher assetEnricher; - private readonly IAssetRepository assetRepository; - private readonly AssetQueryParser queryParser; - - public AssetQueryService( - IAssetEnricher assetEnricher, - IAssetRepository assetRepository, - AssetQueryParser queryParser) - { - Guard.NotNull(assetEnricher, nameof(assetEnricher)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(queryParser, nameof(queryParser)); - - this.assetEnricher = assetEnricher; - this.assetRepository = assetRepository; - this.queryParser = queryParser; - } - - public async Task FindAssetAsync(Context context, Guid id) - { - var asset = await assetRepository.FindAssetAsync(id); - - if (asset != null) - { - return await assetEnricher.EnrichAsync(asset, context); - } - - return null; - } - - public async Task> QueryByHashAsync(Context context, Guid appId, string hash) - { - Guard.NotNull(hash, nameof(hash)); - - var assets = await assetRepository.QueryByHashAsync(appId, hash); - - return await assetEnricher.EnrichAsync(assets, context); - } - - public async Task> QueryAsync(Context context, Q query) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(query, nameof(query)); - - IResultList assets; - - if (query.Ids != null && query.Ids.Count > 0) - { - assets = await QueryByIdsAsync(context, query); - } - else - { - assets = await QueryByQueryAsync(context, query); - } - - var enriched = await assetEnricher.EnrichAsync(assets, context); - - return ResultList.Create(assets.Total, enriched); - } - - private async Task> QueryByQueryAsync(Context context, Q query) - { - var parsedQuery = queryParser.ParseQuery(context, query); - - return await assetRepository.QueryAsync(context.App.Id, parsedQuery); - } - - private async Task> QueryByIdsAsync(Context context, Q query) - { - var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); - - return Sort(assets, query.Ids); - } - - private static IResultList Sort(IResultList assets, IReadOnlyList ids) - { - return assets.SortSet(x => x.Id, ids); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs deleted file mode 100644 index af0f852b5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public sealed class FilterTagTransformer : TransformVisitor - { - private readonly ITagService tagService; - private readonly Guid appId; - - private FilterTagTransformer(Guid appId, ITagService tagService) - { - this.appId = appId; - - this.tagService = tagService; - } - - public static FilterNode Transform(FilterNode nodeIn, Guid appId, ITagService tagService) - { - Guard.NotNull(nodeIn, nameof(nodeIn)); - Guard.NotNull(tagService, nameof(tagService)); - - return nodeIn.Accept(new FilterTagTransformer(appId, tagService)); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) - { - var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; - - if (tagNames.TryGetValue(stringValue, out var normalized)) - { - return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); - } - } - - return nodeIn; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs deleted file mode 100644 index dde7fa42a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Assets.Repositories -{ - public interface IAssetRepository - { - Task> QueryByHashAsync(Guid appId, string hash); - - Task> QueryAsync(Guid appId, ClrQuery query); - - Task> QueryAsync(Guid appId, HashSet ids); - - Task FindAssetAsync(Guid id, bool allowDeleted = false); - - Task FindAssetBySlugAsync(Guid appId, string slug); - - Task RemoveAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs deleted file mode 100644 index 7cf83f469..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ /dev/null @@ -1,262 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Orleans.Concurrency; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.State; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - [Reentrant] - public sealed class BackupGrain : GrainOfGuid, IBackupGrain - { - private const int MaxBackups = 10; - private static readonly Duration UpdateDuration = Duration.FromSeconds(1); - private readonly IAssetStore assetStore; - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IClock clock; - private readonly IJsonSerializer serializer; - private readonly IServiceProvider serviceProvider; - private readonly IEventDataFormatter eventDataFormatter; - private readonly IEventStore eventStore; - private readonly ISemanticLog log; - private readonly IGrainState state; - private CancellationTokenSource currentTask; - private BackupStateJob currentJob; - - public BackupGrain( - IAssetStore assetStore, - IBackupArchiveLocation backupArchiveLocation, - IClock clock, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - IJsonSerializer serializer, - IServiceProvider serviceProvider, - ISemanticLog log, - IGrainState state) - { - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); - Guard.NotNull(serviceProvider, nameof(serviceProvider)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(state, nameof(state)); - Guard.NotNull(log, nameof(log)); - - this.assetStore = assetStore; - this.backupArchiveLocation = backupArchiveLocation; - this.clock = clock; - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.serializer = serializer; - this.serviceProvider = serviceProvider; - this.state = state; - this.log = log; - } - - protected override Task OnActivateAsync(Guid key) - { - RecoverAfterRestartAsync().Forget(); - - return TaskHelper.Done; - } - - private async Task RecoverAfterRestartAsync() - { - foreach (var job in state.Value.Jobs) - { - if (!job.Stopped.HasValue) - { - var jobId = job.Id.ToString(); - - job.Stopped = clock.GetCurrentInstant(); - - await Safe.DeleteAsync(backupArchiveLocation, jobId, log); - await Safe.DeleteAsync(assetStore, jobId, log); - - job.Status = JobStatus.Failed; - - await state.WriteAsync(); - } - } - } - - public async Task RunAsync() - { - if (currentTask != null) - { - throw new DomainException("Another backup process is already running."); - } - - if (state.Value.Jobs.Count >= MaxBackups) - { - throw new DomainException($"You cannot have more than {MaxBackups} backups."); - } - - var job = new BackupStateJob - { - Id = Guid.NewGuid(), - Started = clock.GetCurrentInstant(), - Status = JobStatus.Started - }; - - currentTask = new CancellationTokenSource(); - currentJob = job; - - state.Value.Jobs.Insert(0, job); - - await state.WriteAsync(); - - Process(job, currentTask.Token); - } - - private void Process(BackupStateJob job, CancellationToken ct) - { - ProcessAsync(job, ct).Forget(); - } - - private async Task ProcessAsync(BackupStateJob job, CancellationToken ct) - { - var jobId = job.Id.ToString(); - - var handlers = CreateHandlers(); - - var lastTimestamp = job.Started; - - try - { - using (var stream = await backupArchiveLocation.OpenStreamAsync(jobId)) - { - using (var writer = new BackupWriter(serializer, stream, true)) - { - await eventStore.QueryAsync(async storedEvent => - { - var @event = eventDataFormatter.Parse(storedEvent.Data); - - writer.WriteEvent(storedEvent); - - foreach (var handler in handlers) - { - await handler.BackupEventAsync(@event, Key, writer); - } - - job.HandledEvents = writer.WrittenEvents; - job.HandledAssets = writer.WrittenAttachments; - - lastTimestamp = await WritePeriodically(lastTimestamp); - }, SquidexHeaders.AppId, Key.ToString(), null, ct); - - foreach (var handler in handlers) - { - await handler.BackupAsync(Key, writer); - } - - foreach (var handler in handlers) - { - await handler.CompleteBackupAsync(Key, writer); - } - } - - stream.Position = 0; - - ct.ThrowIfCancellationRequested(); - - await assetStore.UploadAsync(jobId, 0, null, stream, false, ct); - } - - job.Status = JobStatus.Completed; - } - catch (Exception ex) - { - log.LogError(ex, jobId, (ctx, w) => w - .WriteProperty("action", "makeBackup") - .WriteProperty("status", "failed") - .WriteProperty("backupId", ctx)); - - job.Status = JobStatus.Failed; - } - finally - { - await Safe.DeleteAsync(backupArchiveLocation, jobId, log); - - job.Stopped = clock.GetCurrentInstant(); - - await state.WriteAsync(); - - currentTask = null; - currentJob = null; - } - } - - private async Task WritePeriodically(Instant lastTimestamp) - { - var now = clock.GetCurrentInstant(); - - if ((now - lastTimestamp) >= UpdateDuration) - { - lastTimestamp = now; - - await state.WriteAsync(); - } - - return lastTimestamp; - } - - public async Task DeleteAsync(Guid id) - { - var job = state.Value.Jobs.FirstOrDefault(x => x.Id == id); - - if (job == null) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IBackupJob)); - } - - if (currentJob == job) - { - currentTask?.Cancel(); - } - else - { - var jobId = job.Id.ToString(); - - await Safe.DeleteAsync(backupArchiveLocation, jobId, log); - await Safe.DeleteAsync(assetStore, jobId, log); - - state.Value.Jobs.Remove(job); - - await state.WriteAsync(); - } - } - - private IEnumerable CreateHandlers() - { - return serviceProvider.GetRequiredService>(); - } - - public Task>> GetStateAsync() - { - return J.AsTask(state.Value.Jobs.OfType().ToList()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs deleted file mode 100644 index b717f017e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public abstract class BackupHandlerWithStore : BackupHandler - { - private readonly IStore store; - - protected BackupHandlerWithStore(IStore store) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - protected async Task RebuildManyAsync(IEnumerable ids, Func action) - { - foreach (var id in ids) - { - await action(id); - } - } - - protected async Task RebuildAsync(Guid key) where TState : IDomainState, new() - { - var state = new TState - { - Version = EtagVersion.Empty - }; - - var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => - { - state = state.Apply(e); - - state.Version++; - }); - - await persistence.ReadAsync(); - await persistence.WriteSnapshotAsync(state); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs deleted file mode 100644 index 3aa5bc9c5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ /dev/null @@ -1,155 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.Model; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.States; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class BackupReader : DisposableObjectBase - { - private readonly GuidMapper guidMapper = new GuidMapper(); - private readonly ZipArchive archive; - private readonly IJsonSerializer serializer; - private int readEvents; - private int readAttachments; - - public int ReadEvents - { - get { return readEvents; } - } - - public int ReadAttachments - { - get { return readAttachments; } - } - - public BackupReader(IJsonSerializer serializer, Stream stream) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - - archive = new ZipArchive(stream, ZipArchiveMode.Read, false); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - archive.Dispose(); - } - } - - public Guid OldGuid(Guid newId) - { - return guidMapper.OldGuid(newId); - } - - public Task ReadJsonAttachmentAsync(string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); - - if (attachmentEntry == null) - { - throw new FileNotFoundException("Cannot find attachment.", name); - } - - T result; - - using (var stream = attachmentEntry.Open()) - { - result = serializer.Deserialize(stream, null, guidMapper.NewGuidOrValue); - } - - readAttachments++; - - return Task.FromResult(result); - } - - public async Task ReadBlobAsync(string name, Func handler) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(handler, nameof(handler)); - - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); - - if (attachmentEntry == null) - { - throw new FileNotFoundException("Cannot find attachment.", name); - } - - using (var stream = attachmentEntry.Open()) - { - await handler(stream); - } - - readAttachments++; - } - - public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(formatter, nameof(formatter)); - Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - - while (true) - { - var eventEntry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); - - if (eventEntry == null) - { - break; - } - - using (var stream = eventEntry.Open()) - { - var (streamName, data) = serializer.Deserialize(stream).ToEvent(); - - MapHeaders(data); - - var eventStream = streamNameResolver.WithNewId(streamName, guidMapper.NewGuidOrNull); - var eventEnvelope = formatter.Parse(data, guidMapper.NewGuidOrValue); - - await handler((eventStream, eventEnvelope)); - } - - readEvents++; - } - } - - private void MapHeaders(EventData data) - { - foreach (var kvp in data.Headers.ToList()) - { - if (kvp.Value.Type == JsonValueType.String) - { - var newGuid = guidMapper.NewGuidOrNull(kvp.Value.ToString()); - - if (newGuid != null) - { - data.Headers.Add(kvp.Key, newGuid); - } - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs deleted file mode 100644 index 0f3033bcb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.IO.Compression; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.Model; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class BackupWriter : DisposableObjectBase - { - private readonly ZipArchive archive; - private readonly IJsonSerializer serializer; - private readonly Func converter; - private int writtenEvents; - private int writtenAttachments; - - public int WrittenEvents - { - get { return writtenEvents; } - } - - public int WrittenAttachments - { - get { return writtenAttachments; } - } - - public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - - converter = - version == BackupVersion.V1 ? - new Func(CompatibleStoredEvent.V1) : - new Func(CompatibleStoredEvent.V2); - - archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - archive.Dispose(); - } - } - - public Task WriteJsonAsync(string name, object value) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); - - using (var stream = attachmentEntry.Open()) - { - serializer.Serialize(value, stream); - } - - writtenAttachments++; - - return TaskHelper.Done; - } - - public async Task WriteBlobAsync(string name, Func handler) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(handler, nameof(handler)); - - var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); - - using (var stream = attachmentEntry.Open()) - { - await handler(stream); - } - - writtenAttachments++; - } - - public void WriteEvent(StoredEvent storedEvent) - { - Guard.NotNull(storedEvent, nameof(storedEvent)); - - var eventEntry = archive.CreateEntry(ArchiveHelper.GetEventPath(writtenEvents)); - - using (var stream = eventEntry.Open()) - { - var @event = converter(storedEvent); - - serializer.Serialize(@event, stream); - } - - writtenEvents++; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs b/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs deleted file mode 100644 index 830ace6a8..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - internal sealed class GuidMapper - { - private static readonly int GuidLength = Guid.Empty.ToString().Length; - private readonly Dictionary oldToNewGuid = new Dictionary(); - private readonly Dictionary newToOldGuid = new Dictionary(); - private readonly Dictionary strings = new Dictionary(); - - public Guid OldGuid(Guid newGuid) - { - return newToOldGuid.GetOrCreate(newGuid, x => x); - } - - public string NewGuidOrNull(string value) - { - if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) - { - return result; - } - - return null; - } - - public string NewGuidOrValue(string value) - { - if (TryGenerateNewGuidString(value, out var result) || TryGenerateNewNamedId(value, out result)) - { - return result; - } - - return value; - } - - private bool TryGenerateNewGuidString(string value, out string result) - { - if (value.Length == GuidLength) - { - if (strings.TryGetValue(value, out result)) - { - return true; - } - - if (Guid.TryParse(value, out var guid)) - { - var newGuid = GenerateNewGuid(guid); - - strings[value] = result = newGuid.ToString(); - - return true; - } - } - - result = null; - - return false; - } - - private bool TryGenerateNewNamedId(string value, out string result) - { - if (value.Length > GuidLength) - { - if (strings.TryGetValue(value, out result)) - { - return true; - } - - if (NamedId.TryParse(value, Guid.TryParse, out var namedId)) - { - var newGuid = GenerateNewGuid(namedId.Id); - - strings[value] = result = NamedId.Of(newGuid, namedId.Name).ToString(); - - return true; - } - } - - result = null; - - return false; - } - - private Guid GenerateNewGuid(Guid oldGuid) - { - return oldToNewGuid.GetOrAdd(oldGuid, GuidGenerator); - } - - private Guid GuidGenerator(Guid oldGuid) - { - var newGuid = Guid.NewGuid(); - - newToOldGuid[newGuid] = oldGuid; - - return newGuid; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs b/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs deleted file mode 100644 index 5a8aaff9f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/Helpers/Downloader.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Squidex.Infrastructure.Json; - -namespace Squidex.Domain.Apps.Entities.Backup.Helpers -{ - public static class Downloader - { - public static async Task DownloadAsync(this IBackupArchiveLocation backupArchiveLocation, Uri url, string id) - { - if (string.Equals(url.Scheme, "file")) - { - try - { - using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) - { - using (var sourceStream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read)) - { - await sourceStream.CopyToAsync(targetStream); - } - } - } - catch (IOException ex) - { - throw new BackupRestoreException($"Cannot download the archive: {ex.Message}.", ex); - } - } - else - { - HttpResponseMessage response = null; - try - { - using (var client = new HttpClient()) - { - response = await client.GetAsync(url); - response.EnsureSuccessStatusCode(); - - using (var sourceStream = await response.Content.ReadAsStreamAsync()) - { - using (var targetStream = await backupArchiveLocation.OpenStreamAsync(id)) - { - await sourceStream.CopyToAsync(targetStream); - } - } - } - } - catch (HttpRequestException ex) - { - throw new BackupRestoreException($"Cannot download the archive. Got status code: {response?.StatusCode}.", ex); - } - } - } - - public static async Task OpenArchiveAsync(this IBackupArchiveLocation backupArchiveLocation, string id, IJsonSerializer serializer) - { - Stream stream = null; - - try - { - stream = await backupArchiveLocation.OpenStreamAsync(id); - - return new BackupReader(serializer, stream); - } - catch (IOException) - { - stream?.Dispose(); - - throw new BackupRestoreException("The backup archive is correupt and cannot be opened."); - } - catch (Exception) - { - stream?.Dispose(); - - throw; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs deleted file mode 100644 index 1fcc2ffb9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public interface IRestoreGrain : IGrainWithStringKey - { - Task RestoreAsync(Uri url, RefToken actor, string newAppName = null); - - Task> GetJobAsync(); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs deleted file mode 100644 index f7f1f0aef..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ /dev/null @@ -1,367 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Backup.Helpers; -using Squidex.Domain.Apps.Entities.Backup.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class RestoreGrain : GrainOfString, IRestoreGrain - { - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IClock clock; - private readonly ICommandBus commandBus; - private readonly IJsonSerializer serializer; - private readonly IEventStore eventStore; - private readonly IEventDataFormatter eventDataFormatter; - private readonly ISemanticLog log; - private readonly IServiceProvider serviceProvider; - private readonly IStreamNameResolver streamNameResolver; - private readonly IGrainState state; - - private RestoreStateJob CurrentJob - { - get { return state.Value.Job; } - } - - public RestoreGrain(IBackupArchiveLocation backupArchiveLocation, - IClock clock, - ICommandBus commandBus, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - IJsonSerializer serializer, - ISemanticLog log, - IServiceProvider serviceProvider, - IStreamNameResolver streamNameResolver, - IGrainState state) - { - Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(commandBus, nameof(commandBus)); - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); - Guard.NotNull(serializer, nameof(serializer)); - Guard.NotNull(serviceProvider, nameof(serviceProvider)); - Guard.NotNull(state, nameof(state)); - Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - Guard.NotNull(log, nameof(log)); - - this.backupArchiveLocation = backupArchiveLocation; - this.clock = clock; - this.commandBus = commandBus; - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.serializer = serializer; - this.serviceProvider = serviceProvider; - this.streamNameResolver = streamNameResolver; - this.state = state; - this.log = log; - } - - protected override Task OnActivateAsync(string key) - { - RecoverAfterRestartAsync().Forget(); - - return TaskHelper.Done; - } - - private async Task RecoverAfterRestartAsync() - { - if (CurrentJob?.Status == JobStatus.Started) - { - var handlers = CreateHandlers(); - - Log("Failed due application restart"); - - CurrentJob.Status = JobStatus.Failed; - - await CleanupAsync(handlers); - - await state.WriteAsync(); - } - } - - public async Task RestoreAsync(Uri url, RefToken actor, string newAppName) - { - Guard.NotNull(url, nameof(url)); - Guard.NotNull(actor, nameof(actor)); - - if (!string.IsNullOrWhiteSpace(newAppName)) - { - Guard.ValidSlug(newAppName, nameof(newAppName)); - } - - if (CurrentJob?.Status == JobStatus.Started) - { - throw new DomainException("A restore operation is already running."); - } - - state.Value.Job = new RestoreStateJob - { - Id = Guid.NewGuid(), - NewAppName = newAppName, - Actor = actor, - Started = clock.GetCurrentInstant(), - Status = JobStatus.Started, - Url = url - }; - - await state.WriteAsync(); - - Process(); - } - - private void Process() - { - ProcessAsync().Forget(); - } - - private async Task ProcessAsync() - { - var handlers = CreateHandlers(); - - var logContext = (jobId: CurrentJob.Id.ToString(), jobUrl: CurrentJob.Url.ToString()); - - using (Profiler.StartSession()) - { - try - { - Log("Started. The restore process has the following steps:"); - Log(" * Download backup"); - Log(" * Restore events and attachments."); - Log(" * Restore all objects like app, schemas and contents"); - Log(" * Complete the restore operation for all objects"); - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "restore") - .WriteProperty("status", "started") - .WriteProperty("operationId", ctx.jobId) - .WriteProperty("url", ctx.jobUrl)); - - using (Profiler.Trace("Download")) - { - await DownloadAsync(); - } - - using (var reader = await backupArchiveLocation.OpenArchiveAsync(CurrentJob.Id.ToString(), serializer)) - { - using (Profiler.Trace("ReadEvents")) - { - await ReadEventsAsync(reader, handlers); - } - - foreach (var handler in handlers) - { - using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.RestoreAsync))) - { - await handler.RestoreAsync(CurrentJob.AppId, reader); - } - - Log($"Restored {handler.Name}"); - } - - foreach (var handler in handlers) - { - using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.CompleteRestoreAsync))) - { - await handler.CompleteRestoreAsync(CurrentJob.AppId, reader); - } - - Log($"Completed {handler.Name}"); - } - } - - await AssignContributorAsync(); - - CurrentJob.Status = JobStatus.Completed; - - Log("Completed, Yeah!"); - - log.LogInformation(logContext, (ctx, w) => - { - w.WriteProperty("action", "restore"); - w.WriteProperty("status", "completed"); - w.WriteProperty("operationId", ctx.jobId); - w.WriteProperty("url", ctx.jobUrl); - - Profiler.Session?.Write(w); - }); - } - catch (Exception ex) - { - if (ex is BackupRestoreException backupException) - { - Log(backupException.Message); - } - else - { - Log("Failed with internal error"); - } - - await CleanupAsync(handlers); - - CurrentJob.Status = JobStatus.Failed; - - log.LogError(ex, logContext, (ctx, w) => - { - w.WriteProperty("action", "retore"); - w.WriteProperty("status", "failed"); - w.WriteProperty("operationId", ctx.jobId); - w.WriteProperty("url", ctx.jobUrl); - - Profiler.Session?.Write(w); - }); - } - finally - { - CurrentJob.Stopped = clock.GetCurrentInstant(); - - await state.WriteAsync(); - } - } - } - - private async Task AssignContributorAsync() - { - var actor = CurrentJob.Actor; - - if (actor?.IsSubject == true) - { - try - { - await commandBus.PublishAsync(new AssignContributor - { - Actor = actor, - AppId = CurrentJob.AppId, - ContributorId = actor.Identifier, - IsRestore = true, - Role = Role.Owner - }); - - Log("Assigned current user."); - } - catch (DomainException ex) - { - Log($"Failed to assign contributor: {ex.Message}"); - } - } - else - { - Log("Current user not assigned because restore was triggered by client."); - } - } - - private async Task CleanupAsync(IEnumerable handlers) - { - await Safe.DeleteAsync(backupArchiveLocation, CurrentJob.Id.ToString(), log); - - if (CurrentJob.AppId != Guid.Empty) - { - foreach (var handler in handlers) - { - await Safe.CleanupRestoreErrorAsync(handler, CurrentJob.AppId, CurrentJob.Id, log); - } - } - } - - private async Task DownloadAsync() - { - Log("Downloading Backup"); - - await backupArchiveLocation.DownloadAsync(CurrentJob.Url, CurrentJob.Id.ToString()); - - Log("Downloaded Backup"); - } - - private async Task ReadEventsAsync(BackupReader reader, IEnumerable handlers) - { - await reader.ReadEventsAsync(streamNameResolver, eventDataFormatter, async storedEvent => - { - await HandleEventAsync(reader, handlers, storedEvent.Stream, storedEvent.Event); - }); - - Log($"Reading {reader.ReadEvents} events and {reader.ReadAttachments} attachments completed.", true); - } - - private async Task HandleEventAsync(BackupReader reader, IEnumerable handlers, string stream, Envelope @event) - { - if (@event.Payload is SquidexEvent squidexEvent) - { - squidexEvent.Actor = CurrentJob.Actor; - } - - if (@event.Payload is AppCreated appCreated) - { - CurrentJob.AppId = appCreated.AppId.Id; - - if (!string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) - { - appCreated.Name = CurrentJob.NewAppName; - } - } - - if (@event.Payload is AppEvent appEvent && !string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) - { - appEvent.AppId = NamedId.Of(appEvent.AppId.Id, CurrentJob.NewAppName); - } - - foreach (var handler in handlers) - { - if (!await handler.RestoreEventAsync(@event, CurrentJob.AppId, reader, CurrentJob.Actor)) - { - return; - } - } - - var eventData = eventDataFormatter.ToEventData(@event, @event.Headers.CommitId()); - var eventCommit = new List { eventData }; - - await eventStore.AppendAsync(Guid.NewGuid(), stream, eventCommit); - - Log($"Read {reader.ReadEvents} events and {reader.ReadAttachments} attachments.", true); - } - - private void Log(string message, bool replace = false) - { - if (replace && CurrentJob.Log.Count > 0) - { - CurrentJob.Log[CurrentJob.Log.Count - 1] = $"{clock.GetCurrentInstant()}: {message}"; - } - else - { - CurrentJob.Log.Add($"{clock.GetCurrentInstant()}: {message}"); - } - } - - private IEnumerable CreateHandlers() - { - return serviceProvider.GetRequiredService>(); - } - - public Task> GetJobAsync() - { - return Task.FromResult>(CurrentJob); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs b/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs deleted file mode 100644 index 71abbdab7..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Backup.State -{ - [DataContract] - public sealed class RestoreStateJob : IRestoreJob - { - [DataMember] - public string AppName { get; set; } - - [DataMember] - public Guid Id { get; set; } - - [DataMember] - public Guid AppId { get; set; } - - [DataMember] - public RefToken Actor { get; set; } - - [DataMember] - public Uri Url { get; set; } - - [DataMember] - public string NewAppName { get; set; } - - [DataMember] - public Instant Started { get; set; } - - [DataMember] - public Instant? Stopped { get; set; } - - [DataMember] - public List Log { get; set; } = new List(); - - [DataMember] - public JobStatus Status { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs b/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs deleted file mode 100644 index a90a2dc01..000000000 --- a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.Comments.Guards; -using Squidex.Domain.Apps.Entities.Comments.State; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public sealed class CommentsGrain : DomainObjectGrainBase, ICommentsGrain - { - private readonly IStore store; - private readonly List> events = new List>(); - private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty }; - private IPersistence persistence; - - public override CommentsState Snapshot - { - get { return snapshot; } - } - - public CommentsGrain(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - protected override void ApplyEvent(Envelope @event) - { - snapshot = new CommentsState { Version = snapshot.Version + 1 }; - - events.Add(@event.To()); - } - - protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion) - { - snapshot = previousSnapshot; - } - - protected override Task ReadAsync(Type type, Guid id) - { - persistence = store.WithEventSourcing(GetType(), id, ApplyEvent); - - return persistence.ReadAsync(); - } - - protected override async Task WriteAsync(Envelope[] events, long previousVersion) - { - if (events.Length > 0) - { - await persistence.WriteEventsAsync(events); - } - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateComment createComment: - return UpsertReturn(createComment, c => - { - GuardComments.CanCreate(c); - - Create(c); - - return EntityCreatedResult.Create(createComment.CommentId, Version); - }); - - case UpdateComment updateComment: - return Upsert(updateComment, c => - { - GuardComments.CanUpdate(events, c); - - Update(c); - }); - - case DeleteComment deleteComment: - return Upsert(deleteComment, c => - { - GuardComments.CanDelete(events, c); - - Delete(c); - }); - - default: - throw new NotSupportedException(); - } - } - - public void Create(CreateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); - } - - public void Update(UpdateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); - } - - public void Delete(DeleteComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); - } - - public Task GetCommentsAsync(long version = EtagVersion.Any) - { - return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs b/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs deleted file mode 100644 index 1a2921e67..000000000 --- a/src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Comments.Guards -{ - public static class GuardComments - { - public static void CanCreate(CreateComment command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot create comment.", e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined("Text"), nameof(command.Text)); - } - }); - } - - public static void CanUpdate(List> events, UpdateComment command) - { - Guard.NotNull(command, nameof(command)); - - var comment = FindComment(events, command.CommentId); - - if (!comment.Payload.Actor.Equals(command.Actor)) - { - throw new DomainException("Comment is created by another actor."); - } - - Validate.It(() => "Cannot update comment.", e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined("Text"), nameof(command.Text)); - } - }); - } - - public static void CanDelete(List> events, DeleteComment command) - { - Guard.NotNull(command, nameof(command)); - - var comment = FindComment(events, command.CommentId); - - if (!comment.Payload.Actor.Equals(command.Actor)) - { - throw new DomainException("Comment is created by another actor."); - } - } - - private static Envelope FindComment(List> events, Guid commentId) - { - Envelope result = null; - - foreach (var @event in events) - { - if (@event.Payload is CommentCreated created && created.CommentId == commentId) - { - result = @event.To(); - } - else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) - { - result = null; - } - } - - if (result == null) - { - throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain)); - } - - return result; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs deleted file mode 100644 index 30167fe77..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - private readonly IContentLoader contentLoader; - - public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(contentLoader, nameof(contentLoader)); - - this.scriptEngine = scriptEngine; - - this.contentLoader = contentLoader; - } - - protected override async Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedContentEvent(); - - var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber()); - - SimpleMapper.Map(content, result); - - result.Data = content.Data ?? content.DataDraft; - - switch (@event.Payload) - { - case ContentCreated _: - result.Type = EnrichedContentEventType.Created; - break; - case ContentDeleted _: - result.Type = EnrichedContentEventType.Deleted; - break; - case ContentChangesPublished _: - case ContentUpdated _: - result.Type = EnrichedContentEventType.Updated; - break; - case ContentStatusChanged contentStatusChanged: - switch (contentStatusChanged.Change) - { - case StatusChange.Published: - result.Type = EnrichedContentEventType.Published; - break; - case StatusChange.Unpublished: - result.Type = EnrichedContentEventType.Unpublished; - break; - default: - result.Type = EnrichedContentEventType.StatusChanged; - break; - } - - break; - } - - result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; - - return result; - } - - protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId) - { - if (trigger.HandleAll) - { - return true; - } - - if (trigger.Schemas != null) - { - foreach (var schema in trigger.Schemas) - { - if (MatchsSchema(schema, @event.SchemaId)) - { - return true; - } - } - } - - return false; - } - - protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) - { - if (trigger.HandleAll) - { - return true; - } - - if (trigger.Schemas != null) - { - foreach (var schema in trigger.Schemas) - { - if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) - { - return true; - } - } - } - - return false; - } - - private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) - { - return eventId.Id == schema.SchemaId; - } - - private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) - { - return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs deleted file mode 100644 index 4ebe87d7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentCommandMiddleware : GrainCommandMiddleware - { - private readonly IContentEnricher contentEnricher; - private readonly IContextProvider contextProvider; - - public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher, IContextProvider contextProvider) - : base(grainFactory) - { - Guard.NotNull(contentEnricher, nameof(contentEnricher)); - Guard.NotNull(contextProvider, nameof(contextProvider)); - - this.contentEnricher = contentEnricher; - this.contextProvider = contextProvider; - } - - public override async Task HandleAsync(CommandContext context, Func next) - { - await base.HandleAsync(context, next); - - if (context.PlainResult is IContentEntity content && NotEnriched(context)) - { - var enriched = await contentEnricher.EnrichAsync(content, contextProvider.Context); - - context.Complete(enriched); - } - } - - private static bool NotEnriched(CommandContext context) - { - return !(context.PlainResult is IEnrichedContentEntity); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs deleted file mode 100644 index b8c0ded1c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentEntity : IEnrichedContentEntity - { - public Guid Id { get; set; } - - public NamedId AppId { get; set; } - - public NamedId SchemaId { get; set; } - - public long Version { get; set; } - - public Instant Created { get; set; } - - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - - public RefToken LastModifiedBy { get; set; } - - public ScheduleJob ScheduleJob { get; set; } - - public NamedContentData Data { get; set; } - - public NamedContentData DataDraft { get; set; } - - public NamedContentData ReferenceData { get; set; } - - public Status Status { get; set; } - - public StatusInfo[] Nexts { get; set; } - - public string StatusColor { get; set; } - - public string SchemaName { get; set; } - - public string SchemaDisplayName { get; set; } - - public RootField[] ReferenceFields { get; set; } - - public bool CanUpdate { get; set; } - - public bool IsPending { get; set; } - - public HashSet CacheDependencies { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs deleted file mode 100644 index 0de01a4a2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ /dev/null @@ -1,377 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Guards; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentGrain : LogSnapshotDomainObjectGrain, IContentGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - private readonly IAppProvider appProvider; - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; - private readonly IScriptEngine scriptEngine; - private readonly IContentWorkflow contentWorkflow; - - public ContentGrain( - IStore store, - ISemanticLog log, - IAppProvider appProvider, - IAssetRepository assetRepository, - IScriptEngine scriptEngine, - IContentWorkflow contentWorkflow, - IContentRepository contentRepository, - IActivationLimit limit) - : base(store, log) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); - Guard.NotNull(contentRepository, nameof(contentRepository)); - - this.appProvider = appProvider; - this.scriptEngine = scriptEngine; - this.assetRepository = assetRepository; - this.contentWorkflow = contentWorkflow; - this.contentRepository = contentRepository; - - limit?.SetLimit(5000, Lifetime); - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case CreateContent createContent: - return CreateReturnAsync(createContent, async c => - { - var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, c, () => "Failed to create content."); - - var status = (await contentWorkflow.GetInitialStatusAsync(ctx.Schema)).Status; - - await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); - - c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, - new ScriptContext - { - Operation = "Create", - Data = c.Data, - Status = status, - StatusOld = default - }); - - await ctx.EnrichAsync(c.Data); - - if (!c.DoNotValidate) - { - await ctx.ValidateAsync(c.Data); - } - - if (c.Publish) - { - await ctx.ExecuteScriptAsync(s => s.Change, - new ScriptContext - { - Operation = "Published", - Data = c.Data, - Status = Status.Published, - StatusOld = default - }); - } - - Create(c, status); - - return Snapshot; - }); - - case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, async c => - { - var isProposal = c.AsDraft && Snapshot.Status == Status.Published; - - await GuardContent.CanUpdate(Snapshot, contentWorkflow, c, isProposal); - - return await UpdateAsync(c, x => c.Data, false, isProposal); - }); - - case PatchContent patchContent: - return UpdateReturnAsync(patchContent, async c => - { - var isProposal = IsProposal(c); - - await GuardContent.CanPatch(Snapshot, contentWorkflow, c, isProposal); - - return await UpdateAsync(c, c.Data.MergeInto, true, isProposal); - }); - - case ChangeContentStatus changeContentStatus: - return UpdateReturnAsync(changeContentStatus, async c => - { - try - { - var isChangeConfirm = IsConfirm(c); - - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to change content."); - - await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c, isChangeConfirm); - - if (c.DueTime.HasValue) - { - ScheduleStatus(c); - } - else - { - if (isChangeConfirm) - { - ConfirmChanges(c); - } - else - { - var change = GetChange(c); - - await ctx.ExecuteScriptAsync(s => s.Change, - new ScriptContext - { - Operation = change.ToString(), - Data = Snapshot.Data, - Status = c.Status, - StatusOld = Snapshot.Status - }); - - ChangeStatus(c, change); - } - } - } - catch (Exception) - { - if (c.JobId.HasValue && Snapshot?.ScheduleJob.Id == c.JobId) - { - CancelScheduling(c); - } - else - { - throw; - } - } - - return Snapshot; - }); - - case DiscardChanges discardChanges: - return UpdateReturn(discardChanges, c => - { - GuardContent.CanDiscardChanges(Snapshot.IsPending, c); - - DiscardChanges(c); - - return Snapshot; - }); - - case DeleteContent deleteContent: - return UpdateAsync(deleteContent, async c => - { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete content."); - - GuardContent.CanDelete(ctx.Schema, c); - - await ctx.ExecuteScriptAsync(s => s.Delete, - new ScriptContext - { - Operation = "Delete", - Data = Snapshot.Data, - Status = Snapshot.Status, - StatusOld = default - }); - - Delete(c); - }); - - default: - throw new NotSupportedException(); - } - } - - private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial, bool isProposal) - { - var currentData = - isProposal ? - Snapshot.DataDraft : - Snapshot.Data; - - var newData = newDataFunc(currentData); - - if (!currentData.Equals(newData)) - { - var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, command, () => "Failed to update content."); - - if (partial) - { - await ctx.ValidatePartialAsync(command.Data); - } - else - { - await ctx.ValidateAsync(command.Data); - } - - newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, - new ScriptContext - { - Operation = "Create", - Data = newData, - DataOld = currentData, - Status = Snapshot.Status, - StatusOld = default - }); - - if (isProposal) - { - ProposeUpdate(command, newData); - } - else - { - Update(command, newData); - } - } - - return Snapshot; - } - - public void Create(CreateContent command, Status status) - { - RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); - - if (command.Publish) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); - } - } - - public void ConfirmChanges(ChangeContentStatus command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentChangesPublished())); - } - - public void DiscardChanges(DiscardChanges command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentChangesDiscarded())); - } - - public void Delete(DeleteContent command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); - } - - public void Update(ContentCommand command, NamedContentData data) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = data })); - } - - public void ProposeUpdate(ContentCommand command, NamedContentData data) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdateProposed { Data = data })); - } - - public void CancelScheduling(ChangeContentStatus command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentSchedulingCancelled())); - } - - public void ScheduleStatus(ChangeContentStatus command) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); - } - - public void ChangeStatus(ChangeContentStatus command, StatusChange change) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change })); - } - - private void RaiseEvent(SchemaEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - if (@event.SchemaId == null) - { - @event.SchemaId = Snapshot.SchemaId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private bool IsConfirm(ChangeContentStatus command) - { - return Snapshot.IsPending && Snapshot.Status == Status.Published && command.Status == Status.Published; - } - - private bool IsProposal(PatchContent command) - { - return Snapshot.Status == Status.Published && command.AsDraft; - } - - private StatusChange GetChange(ChangeContentStatus command) - { - var change = StatusChange.Change; - - if (command.Status == Status.Published) - { - change = StatusChange.Published; - } - else if (Snapshot.Status == Status.Published) - { - change = StatusChange.Unpublished; - } - - return change; - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Content has already been deleted."); - } - } - - private async Task CreateContext(Guid appId, Guid schemaId, ContentCommand command, Func message) - { - var operationContext = - await ContentOperationContext.CreateAsync(appId, schemaId, command, - appProvider, assetRepository, contentRepository, scriptEngine, message); - - return operationContext; - } - - public Task> GetStateAsync(long version = EtagVersion.Any) - { - return J.AsTask(GetSnapshot(version)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs deleted file mode 100644 index c94213680..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase - { - public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "created {[Schema]} content."); - - AddEventMessage( - "updated {[Schema]} content."); - - AddEventMessage( - "deleted {[Schema]} content."); - - AddEventMessage( - "discarded pending changes of {[Schema]} content."); - - AddEventMessage( - "published changes of {[Schema]} content."); - - AddEventMessage( - "proposed update for {[Schema]} content."); - - AddEventMessage( - "failed to schedule status change for {[Schema]} content."); - - AddEventMessage( - "changed status of {[Schema]} content to {[Status]}."); - - AddEventMessage( - "scheduled to change status of {[Schema]} content to {[Status]}."); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - var channel = $"contents.{@event.Headers.AggregateId()}"; - - var result = ForEvent(@event.Payload, channel); - - if (@event.Payload is SchemaEvent schemaEvent) - { - result = result.Param("Schema", schemaEvent.SchemaId.Name); - } - - if (@event.Payload is ContentStatusChanged contentStatusChanged) - { - result = result.Param("Status", contentStatusChanged.Status); - } - - if (@event.Payload is ContentStatusScheduled contentStatusScheduled) - { - result = result.Param("Status", contentStatusScheduled.Status); - } - - return Task.FromResult(result); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs deleted file mode 100644 index d5752543a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ /dev/null @@ -1,143 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.EnrichContent; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentOperationContext - { - private IContentRepository contentRepository; - private IAssetRepository assetRepository; - private IScriptEngine scriptEngine; - private ISchemaEntity schemaEntity; - private IAppEntity appEntity; - private ContentCommand command; - private Guid schemaId; - private Func message; - - public ISchemaEntity Schema - { - get { return schemaEntity; } - } - - public static async Task CreateAsync( - Guid appId, - Guid schemaId, - ContentCommand command, - IAppProvider appProvider, - IAssetRepository assetRepository, - IContentRepository contentRepository, - IScriptEngine scriptEngine, - Func message) - { - var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(appId, schemaId); - - var context = new ContentOperationContext - { - appEntity = appEntity, - assetRepository = assetRepository, - command = command, - contentRepository = contentRepository, - message = message, - schemaId = schemaId, - schemaEntity = schemaEntity, - scriptEngine = scriptEngine - }; - - return context; - } - - public Task EnrichAsync(NamedContentData data) - { - data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); - - return TaskHelper.Done; - } - - public Task ValidateAsync(NamedContentData data) - { - var ctx = CreateValidationContext(); - - return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); - } - - public Task ValidatePartialAsync(NamedContentData data) - { - var ctx = CreateValidationContext(); - - return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); - } - - public Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) - { - Enrich(context); - - var result = scriptEngine.ExecuteAndTransform(context, GetScript(script)); - - return Task.FromResult(result); - } - - public Task ExecuteScriptAsync(Func script, ScriptContext context) - { - Enrich(context); - - scriptEngine.Execute(context, GetScript(script)); - - return TaskHelper.Done; - } - - private void Enrich(ScriptContext context) - { - context.ContentId = command.ContentId; - - context.User = command.User; - } - - private ValidationContext CreateValidationContext() - { - return new ValidationContext(command.ContentId, schemaId, - QueryContentsAsync, - QueryContentsAsync, - QueryAssetsAsync); - } - - private async Task> QueryAssetsAsync(IEnumerable assetIds) - { - return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); - } - - private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) - { - return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); - } - - private async Task> QueryContentsAsync(HashSet ids) - { - return await contentRepository.QueryIdsAsync(appEntity.Id, ids); - } - - private string GetScript(Func script) - { - return script(schemaEntity.SchemaDef.Scripts); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs deleted file mode 100644 index 051f66a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// 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 NodaTime; -using Orleans; -using Orleans.Runtime; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable - { - private readonly IContentRepository contentRepository; - private readonly ICommandBus commandBus; - private readonly IClock clock; - private readonly ISemanticLog log; - private TaskScheduler scheduler; - - public ContentSchedulerGrain( - IContentRepository contentRepository, - ICommandBus commandBus, - IClock clock, - ISemanticLog log) - { - Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(commandBus, nameof(commandBus)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.clock = clock; - - this.commandBus = commandBus; - this.contentRepository = contentRepository; - - this.log = log; - } - - public override Task OnActivateAsync() - { - scheduler = TaskScheduler.Current; - - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => PublishAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); - } - - public Task ActivateAsync() - { - return TaskHelper.Done; - } - - public Task PublishAsync() - { - var now = clock.GetCurrentInstant(); - - return contentRepository.QueryScheduledWithoutDataAsync(now, content => - { - return Dispatch(async () => - { - try - { - var job = content.ScheduleJob; - - if (job != null) - { - var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id }; - - await commandBus.PublishAsync(command); - } - } - catch (Exception ex) - { - log.LogError(ex, content.Id.ToString(), (logContentId, w) => w - .WriteProperty("action", "ChangeStatusScheduled") - .WriteProperty("status", "Failed") - .WriteProperty("contentId", logContentId)); - } - }); - }); - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - - private Task Dispatch(Func task) - { - return Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler ?? TaskScheduler.Default).Unwrap(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs deleted file mode 100644 index 75ddd704b..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class DefaultWorkflowsValidator : IWorkflowsValidator - { - private readonly IAppProvider appProvider; - - public DefaultWorkflowsValidator(IAppProvider appProvider) - { - Guard.NotNull(appProvider, nameof(appProvider)); - - this.appProvider = appProvider; - } - - public async Task> ValidateAsync(Guid appId, Workflows workflows) - { - Guard.NotNull(workflows, nameof(workflows)); - - var errors = new List(); - - if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) - { - errors.Add("Multiple workflows cover all schemas."); - } - - var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); - - foreach (var schemaId in uniqueSchemaIds) - { - if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) - { - var schema = await appProvider.GetSchemaAsync(appId, schemaId); - - if (schema != null) - { - errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows."); - } - } - } - - return errors; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs deleted file mode 100644 index 6ae3f9e58..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class DynamicContentWorkflow : IContentWorkflow - { - private readonly IScriptEngine scriptEngine; - private readonly IAppProvider appProvider; - - public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.scriptEngine = scriptEngine; - - this.appProvider = appProvider; - } - - public async Task GetAllAsync(ISchemaEntity schema) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - - return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); - } - - public async Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user); - } - - public async Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - - return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user); - } - - public async Task CanUpdateAsync(IContentEntity content) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - if (workflow.TryGetStep(content.Status, out var step)) - { - return !step.NoUpdate; - } - - return true; - } - - public async Task GetInfoAsync(IContentEntity content) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - if (workflow.TryGetStep(content.Status, out var step)) - { - return new StatusInfo(content.Status, GetColor(step)); - } - - return new StatusInfo(content.Status, StatusColors.Draft); - } - - public async Task GetInitialStatusAsync(ISchemaEntity schema) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - - var (status, step) = workflow.GetInitialStep(); - - return new StatusInfo(status, GetColor(step)); - } - - public async Task GetNextsAsync(IContentEntity content, ClaimsPrincipal user) - { - var result = new List(); - - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) - { - if (CanUse(transition, content.DataDraft, user)) - { - result.Add(new StatusInfo(to, GetColor(step))); - } - } - - return result.ToArray(); - } - - private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user) - { - if (transition.Roles != null) - { - if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && transition.Roles.Contains(x.Value))) - { - return false; - } - } - - if (!string.IsNullOrWhiteSpace(transition.Expression)) - { - return scriptEngine.Evaluate("data", data, transition.Expression); - } - - return true; - } - - private async Task GetWorkflowAsync(Guid appId, Guid schemaId) - { - Workflow result = null; - - var app = await appProvider.GetAppAsync(appId); - - if (app != null) - { - result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); - - if (result == null) - { - result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); - } - } - - if (result == null) - { - result = Workflow.Default; - } - - return result; - } - - private static string GetColor(WorkflowStep step) - { - return step.Color ?? StatusColors.Draft; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs deleted file mode 100644 index da2e74af8..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using GraphQL; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IDependencyResolver resolver; - - public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) - : base(cache) - { - Guard.NotNull(resolver, nameof(resolver)); - - this.resolver = resolver; - } - - public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(queries, nameof(queries)); - - var model = await GetModelAsync(context.App); - - var ctx = new GraphQLExecutionContext(context, resolver); - - var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); - - return (result.Any(x => x.HasError), result.Map(x => x.Response)); - } - - public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(query, nameof(query)); - - var model = await GetModelAsync(context.App); - - var ctx = new GraphQLExecutionContext(context, resolver); - - var result = await QueryInternalAsync(model, ctx, query); - - return result; - } - - private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) - { - if (string.IsNullOrWhiteSpace(query.Query)) - { - return (false, new { data = new object() }); - } - - var (data, errors) = await model.ExecuteAsync(ctx, query); - - if (errors?.Any() == true) - { - return (false, new { data, errors }); - } - else - { - return (false, new { data }); - } - } - - private Task GetModelAsync(IAppEntity app) - { - var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); - - return Cache.GetOrCreateAsync(cacheKey, async entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); - - return new GraphQLModel(app, - allSchemas, - GetPageSizeForContents(), - GetPageSizeForAssets(), - resolver.Resolve()); - }); - } - - private int GetPageSizeForContents() - { - return resolver.Resolve>().Value.DefaultPageSizeGraphQl; - } - - private int GetPageSizeForAssets() - { - return resolver.Resolve>().Value.DefaultPageSizeGraphQl; - } - - private static object CreateCacheKey(Guid appId, string etag) - { - return $"GraphQLModel_{appId}_{etag}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs deleted file mode 100644 index f5a27ffd9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using GraphQL; -using GraphQL.DataLoader; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Contents.Queries; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public sealed class GraphQLExecutionContext : QueryExecutionContext - { - private static readonly List EmptyAssets = new List(); - private static readonly List EmptyContents = new List(); - private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; - private readonly IDependencyResolver resolver; - - public IGraphQLUrlGenerator UrlGenerator { get; } - - public ISemanticLog Log { get; } - - public GraphQLExecutionContext(Context context, IDependencyResolver resolver) - : base(context, - resolver.Resolve(), - resolver.Resolve()) - { - UrlGenerator = resolver.Resolve(); - - dataLoaderContextAccessor = resolver.Resolve(); - - this.resolver = resolver; - } - - public void Setup(ExecutionOptions execution) - { - var loader = resolver.Resolve(); - - execution.Listeners.Add(loader); - execution.FieldMiddleware.Use(Middlewares.Logging(resolver.Resolve())); - execution.FieldMiddleware.Use(Middlewares.Errors()); - - execution.UserContext = this; - } - - public override Task FindAssetAsync(Guid id) - { - var dataLoader = GetAssetsLoader(); - - return dataLoader.LoadAsync(id); - } - - public Task FindContentAsync(Guid id) - { - var dataLoader = GetContentsLoader(); - - return dataLoader.LoadAsync(id); - } - - public async Task> GetReferencedAssetsAsync(IJsonValue value) - { - var ids = ParseIds(value); - - if (ids == null) - { - return EmptyAssets; - } - - var dataLoader = GetAssetsLoader(); - - return await dataLoader.LoadManyAsync(ids); - } - - public async Task> GetReferencedContentsAsync(IJsonValue value) - { - var ids = ParseIds(value); - - if (ids == null) - { - return EmptyContents; - } - - var dataLoader = GetContentsLoader(); - - return await dataLoader.LoadManyAsync(ids); - } - - private IDataLoader GetAssetsLoader() - { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", - async batch => - { - var result = await GetReferencedAssetsAsync(new List(batch)); - - return result.ToDictionary(x => x.Id); - }); - } - - private IDataLoader GetContentsLoader() - { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"References", - async batch => - { - var result = await GetReferencedContentsAsync(new List(batch)); - - return result.ToDictionary(x => x.Id); - }); - } - - private static ICollection ParseIds(IJsonValue value) - { - try - { - var result = new List(); - - if (value is JsonArray array) - { - foreach (var id in array) - { - result.Add(Guid.Parse(id.ToString())); - } - } - - return result; - } - catch - { - return null; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs deleted file mode 100644 index 151264b20..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ /dev/null @@ -1,180 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using GraphQL; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using GraphQLSchema = GraphQL.Types.Schema; - -#pragma warning disable IDE0003 - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public sealed class GraphQLModel : IGraphModel - { - private readonly Dictionary contentTypes = new Dictionary(); - private readonly PartitionResolver partitionResolver; - private readonly IAppEntity app; - private readonly IGraphType assetType; - private readonly IGraphType assetListType; - private readonly GraphQLSchema graphQLSchema; - - public bool CanGenerateAssetSourceUrl { get; } - - public GraphQLModel(IAppEntity app, - IEnumerable schemas, - int pageSizeContents, - int pageSizeAssets, - IGraphQLUrlGenerator urlGenerator) - { - this.app = app; - - partitionResolver = app.PartitionResolver(); - - CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; - - assetType = new AssetGraphType(this); - assetListType = new ListGraphType(new NonNullGraphType(assetType)); - - var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); - - BuildSchemas(allSchemas); - - graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); - graphQLSchema.RegisterValueConverter(JsonConverter.Instance); - - InitializeContentTypes(); - } - - private void BuildSchemas(List allSchemas) - { - foreach (var schema in allSchemas) - { - contentTypes[schema.Id] = new ContentGraphType(schema); - } - } - - private void InitializeContentTypes() - { - foreach (var contentType in contentTypes.Values) - { - contentType.Initialize(this); - } - - foreach (var contentType in contentTypes.Values) - { - graphQLSchema.RegisterType(contentType); - } - } - - private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List schemas) - { - var schema = new GraphQLSchema - { - Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) - }; - - return schema; - } - - public IFieldResolver ResolveAssetUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetSourceUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetThumbnailUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveContentUrl(ISchemaEntity schema) - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); - }); - - return resolver; - } - - public IFieldPartitioning ResolvePartition(Partitioning key) - { - return partitionResolver(key); - } - - public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) - { - return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); - } - - public IObjectGraphType GetAssetType() - { - return assetType as IObjectGraphType; - } - - public IObjectGraphType GetContentType(Guid schemaId) - { - return contentTypes.GetOrDefault(schemaId); - } - - public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) - { - Guard.NotNull(context, nameof(context)); - - var result = await new DocumentExecuter().ExecuteAsync(execution => - { - context.Setup(execution); - - execution.Schema = graphQLSchema; - execution.Inputs = query.Variables?.ToInputs(); - execution.Query = query.Query; - }).ConfigureAwait(false); - - return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs deleted file mode 100644 index d945c3a83..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public interface IGraphModel - { - bool CanGenerateAssetSourceUrl { get; } - - IFieldPartitioning ResolvePartition(Partitioning key); - - IFieldResolver ResolveAssetUrl(); - - IFieldResolver ResolveAssetSourceUrl(); - - IFieldResolver ResolveAssetThumbnailUrl(); - - IFieldResolver ResolveContentUrl(ISchemaEntity schema); - - IObjectGraphType GetAssetType(); - - IObjectGraphType GetContentType(Guid schemaId); - - (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs deleted file mode 100644 index 48afc4d4e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public interface IGraphQLUrlGenerator - { - bool CanGenerateAssetSourceUrl { get; } - - string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); - - string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); - - string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); - - string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs deleted file mode 100644 index 03adccccc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL; -using GraphQL.Instrumentation; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public static class Middlewares - { - public static Func Logging(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - return next => - { - return async context => - { - try - { - return await next(context); - } - catch (Exception ex) - { - log.LogWarning(ex, w => w - .WriteProperty("action", "reolveField") - .WriteProperty("status", "failed") - .WriteProperty("field", context.FieldName)); - - throw; - } - }; - }; - } - - public static Func Errors() - { - return next => - { - return async context => - { - try - { - return await next(context); - } - catch (DomainException ex) - { - throw new ExecutionError(ex.Message); - } - }; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs deleted file mode 100644 index 639ee6d55..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ /dev/null @@ -1,194 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class AssetGraphType : ObjectGraphType - { - public AssetGraphType(IGraphModel model) - { - Name = "Asset"; - - AddField(new FieldType - { - Name = "id", - ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id.ToString()), - Description = "The id of the asset." - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = "The version of the asset." - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), - Description = "The date and time when the asset has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = "The user that has created the asset." - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), - Description = "The date and time when the asset has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = "The user that has updated the asset last." - }); - - AddField(new FieldType - { - Name = "mimeType", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.MimeType), - Description = "The mime type." - }); - - AddField(new FieldType - { - Name = "url", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveAssetUrl(), - Description = "The url to the asset." - }); - - AddField(new FieldType - { - Name = "thumbnailUrl", - ResolvedType = AllTypes.String, - Resolver = model.ResolveAssetThumbnailUrl(), - Description = "The thumbnail url to the asset." - }); - - AddField(new FieldType - { - Name = "fileName", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName), - Description = "The file name." - }); - - AddField(new FieldType - { - Name = "fileHash", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileHash), - Description = "The hash of the file. Can be null for old files." - }); - - AddField(new FieldType - { - Name = "fileType", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName.FileType()), - Description = "The file type." - }); - - AddField(new FieldType - { - Name = "fileSize", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.FileSize), - Description = "The size of the file in bytes." - }); - - AddField(new FieldType - { - Name = "fileVersion", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.FileVersion), - Description = "The version of the file." - }); - - AddField(new FieldType - { - Name = "slug", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Slug), - Description = "The file name as slug." - }); - - AddField(new FieldType - { - Name = "isImage", - ResolvedType = AllTypes.NonNullBoolean, - Resolver = Resolve(x => x.IsImage), - Description = "Determines of the created file is an image." - }); - - AddField(new FieldType - { - Name = "pixelWidth", - ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.PixelWidth), - Description = "The width of the image in pixels if the asset is an image." - }); - - AddField(new FieldType - { - Name = "pixelHeight", - ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.PixelHeight), - Description = "The height of the image in pixels if the asset is an image." - }); - - AddField(new FieldType - { - Name = "tags", - ResolvedType = null, - Resolver = Resolve(x => x.TagNames), - Description = "The asset tags.", - Type = AllTypes.NonNullTagsType - }); - - if (model.CanGenerateAssetSourceUrl) - { - AddField(new FieldType - { - Name = "sourceUrl", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveAssetSourceUrl(), - Description = "The source url of the asset." - }); - } - - Description = "An asset"; - } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs deleted file mode 100644 index 517776b3f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentDataGraphType : ObjectGraphType - { - public ContentDataGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) - { - Name = $"{schemaType}DataDto"; - - foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) - { - var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); - - if (valueResolver != null) - { - var displayName = field.DisplayName(); - - var fieldGraphType = new ObjectGraphType - { - Name = $"{schemaType}Data{typeName}Dto" - }; - - var partition = model.ResolvePartition(field.Partitioning); - - foreach (var partitionItem in partition) - { - var key = partitionItem.Key; - - fieldGraphType.AddField(new FieldType - { - Name = key.EscapePartition(), - Resolver = PartitionResolver(valueResolver, key), - ResolvedType = resolvedType, - Description = field.RawProperties.Hints - }); - } - - fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content type."; - - AddField(new FieldType - { - Name = fieldName, - Resolver = FieldResolver(field), - ResolvedType = fieldGraphType, - Description = $"The {displayName} field." - }); - } - } - - Description = $"The structure of the {schemaName} content type."; - } - - private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) - { - return new FuncFieldResolver(c => - { - if (((ContentFieldData)c.Source).TryGetValue(key, out var value)) - { - return valueResolver(value, c); - } - else - { - return null; - } - }); - } - - private static FuncFieldResolver> FieldResolver(RootField field) - { - return new FuncFieldResolver>(c => - { - return c.Source.GetOrDefault(field.Name); - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs deleted file mode 100644 index d07ee4b82..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Entities.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentGraphType : ObjectGraphType - { - private readonly ISchemaEntity schema; - private readonly string schemaType; - private readonly string schemaName; - - public ContentGraphType(ISchemaEntity schema) - { - this.schema = schema; - - schemaType = schema.TypeName(); - schemaName = schema.DisplayName(); - - Name = $"{schemaType}"; - - AddField(new FieldType - { - Name = "id", - ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id), - Description = $"The id of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = $"The version of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), - Description = $"The date and time when the {schemaName} content has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = $"The user that has created the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), - Description = $"The date and time when the {schemaName} content has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = $"The user that has updated the {schemaName} content last." - }); - - AddField(new FieldType - { - Name = "status", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), - Description = $"The the status of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "statusColor", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.StatusColor), - Description = $"The color status of the {schemaName} content." - }); - - Interface(); - - Description = $"The structure of a {schemaName} content type."; - - IsTypeOf = CheckType; - } - - private bool CheckType(object value) - { - return value is IContentEntity content && content.SchemaId?.Id == schema.Id; - } - - public void Initialize(IGraphModel model) - { - AddField(new FieldType - { - Name = "url", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveContentUrl(schema), - Description = $"The url to the the {schemaName} content." - }); - - var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model); - - if (contentDataType.Fields.Any()) - { - AddField(new FieldType - { - Name = "data", - ResolvedType = new NonNullGraphType(contentDataType), - Resolver = Resolve(x => x.Data), - Description = $"The data of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "dataDraft", - ResolvedType = contentDataType, - Resolver = Resolve(x => x.DataDraft), - Description = $"The draft data of the {schemaName} content." - }); - } - } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs deleted file mode 100644 index 523b58032..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQL.Types; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentUnionGraphType : UnionGraphType - { - private readonly Dictionary types = new Dictionary(); - - public ContentUnionGraphType(string fieldName, Dictionary schemaTypes, IEnumerable schemaIds) - { - Name = $"{fieldName}ReferenceUnionDto"; - - if (schemaIds?.Any() == true) - { - foreach (var schemaId in schemaIds) - { - var schemaType = schemaTypes.GetOrDefault(schemaId); - - if (schemaType != null) - { - types[schemaId] = schemaType; - } - } - } - else - { - foreach (var schemaType in schemaTypes) - { - types[schemaType.Key] = schemaType.Value; - } - } - - foreach (var type in types) - { - AddPossibleType(type.Value); - } - - ResolveType = value => - { - if (value is IContentEntity content) - { - return types.GetOrDefault(content.SchemaId.Id); - } - - return null; - }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs deleted file mode 100644 index cd23927b6..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class NestedGraphType : ObjectGraphType - { - public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName) - { - var schemaType = schema.TypeName(); - var schemaName = schema.DisplayName(); - - var fieldDisplayName = field.DisplayName(); - - Name = $"{schemaType}{fieldName}ChildDto"; - - foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) - { - var fieldInfo = model.GetGraphType(schema, nestedField, nestedName); - - if (fieldInfo.ResolveType != null) - { - var resolver = ValueResolver(nestedField, fieldInfo); - - AddField(new FieldType - { - Name = nestedName, - Resolver = resolver, - ResolvedType = fieldInfo.ResolveType, - Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." - }); - } - } - - Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; - } - - private static FuncFieldResolver ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo) - { - return new FuncFieldResolver(c => - { - if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value)) - { - return fieldInfo.Resolver(value, c); - } - else - { - return fieldInfo; - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs deleted file mode 100644 index e038b0432..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context); - - public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, ValueResolver Resolver)> - { - private static readonly ValueResolver NoopResolver = (value, c) => value; - private readonly Dictionary schemaTypes; - private readonly ISchemaEntity schema; - private readonly IGraphModel model; - private readonly IGraphType assetListType; - private readonly string fieldName; - - public QueryGraphTypeVisitor(ISchemaEntity schema, - Dictionary schemaTypes, - IGraphModel model, - IGraphType assetListType, - string fieldName) - { - this.model = model; - this.assetListType = assetListType; - this.schema = schema; - this.schemaTypes = schemaTypes; - this.fieldName = fieldName; - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IArrayField field) - { - return ResolveNested(field); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveAssets(); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopBoolean); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopDate); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopGeolocation); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopJson); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopFloat); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveReferences(field); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopString); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return ResolveDefault(AllTypes.NoopTags); - } - - public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) - { - return (null, null); - } - - private static (IGraphType ResolveType, ValueResolver Resolver) ResolveDefault(IGraphType type) - { - return (type, NoopResolver); - } - - private (IGraphType ResolveType, ValueResolver Resolver) ResolveNested(IArrayField field) - { - var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); - - return (schemaFieldType, NoopResolver); - } - - private (IGraphType ResolveType, ValueResolver Resolver) ResolveAssets() - { - var resolver = new ValueResolver((value, c) => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.GetReferencedAssetsAsync(value); - }); - - return (assetListType, resolver); - } - - private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field) - { - IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); - - if (contentType == null) - { - var union = new ContentUnionGraphType(fieldName, schemaTypes, field.Properties.SchemaIds); - - if (!union.PossibleTypes.Any()) - { - return (null, null); - } - - contentType = union; - } - - var resolver = new ValueResolver((value, c) => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.GetReferencedContentsAsync(value); - }); - - var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); - - return (schemaFieldType, resolver); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs deleted file mode 100644 index 1245e857f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Language.AST; -using GraphQL.Types; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class GuidGraphType2 : ScalarGraphType - { - public GuidGraphType2() - { - Name = "Guid"; - - Description = "The `Guid` scalar type global unique identifier"; - } - - public override object Serialize(object value) - { - return ParseValue(value)?.ToString(); - } - - public override object ParseValue(object value) - { - if (value is Guid guid) - { - return guid; - } - - var inputValue = value?.ToString().Trim('"'); - - if (Guid.TryParse(inputValue, out guid)) - { - return guid; - } - - return null; - } - - public override object ParseLiteral(IValue value) - { - if (value is StringValue stringValue) - { - return ParseValue(stringValue.Value); - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs deleted file mode 100644 index 2f45fe6a9..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using GraphQL.Types; -using NodaTime.Text; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class InstantGraphType : DateGraphType - { - public override object Serialize(object value) - { - return ParseValue(value); - } - - public override object ParseValue(object value) - { - return InstantPattern.General.Parse(value.ToString()).Value; - } - - public override object ParseLiteral(IValue value) - { - if (value is InstantValue timeValue) - { - return ParseValue(timeValue.Value); - } - - if (value is StringValue stringValue) - { - return ParseValue(stringValue.Value); - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs deleted file mode 100644 index a433a5fb3..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using GraphQL.Types; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class JsonConverter : IAstFromValueConverter - { - public static readonly JsonConverter Instance = new JsonConverter(); - - private JsonConverter() - { - } - - public IValue Convert(object value, IGraphType type) - { - return new JsonValue(value as JsonObject); - } - - public bool Matches(object value, IGraphType type) - { - return value is JsonObject; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs deleted file mode 100644 index 3b99c8412..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using GraphQL.Types; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class JsonGraphType : ScalarGraphType - { - public JsonGraphType() - { - Name = "Json"; - - Description = "Unstructured Json object"; - } - - public override object Serialize(object value) - { - return value; - } - - public override object ParseValue(object value) - { - return value; - } - - public override object ParseLiteral(IValue value) - { - if (value is JsonValue jsonGraphType) - { - return jsonGraphType.Value; - } - - return value; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs deleted file mode 100644 index 01449380f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.Language.AST; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class JsonValue : ValueNode - { - public JsonValue(IJsonValue value) - { - Value = value; - } - - protected override bool Equals(ValueNode node) - { - return false; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs deleted file mode 100644 index e7c19aac1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Contents.Guards -{ - public static class GuardContent - { - public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot created content.", e => - { - ValidateData(command, e); - }); - - if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id) - { - throw new DomainException("Singleton content cannot be created."); - } - - if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User)) - { - throw new DomainException("Content workflow prevents publishing."); - } - } - - public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command, bool isProposal) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot update content.", e => - { - ValidateData(command, e); - }); - - if (!isProposal) - { - await ValidateCanUpdate(content, contentWorkflow); - } - } - - public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command, bool isProposal) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot patch content.", e => - { - ValidateData(command, e); - }); - - if (!isProposal) - { - await ValidateCanUpdate(content, contentWorkflow); - } - } - - public static void CanDiscardChanges(bool isPending, DiscardChanges command) - { - Guard.NotNull(command, nameof(command)); - - if (!isPending) - { - throw new DomainException("The content has no pending changes."); - } - } - - public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command, bool isChangeConfirm) - { - Guard.NotNull(command, nameof(command)); - - if (schema.SchemaDef.IsSingleton && command.Status != Status.Published) - { - throw new DomainException("Singleton content cannot be changed."); - } - - return Validate.It(() => "Cannot change status.", async e => - { - if (isChangeConfirm) - { - if (!content.IsPending) - { - e("Content has no changes to publish.", nameof(command.Status)); - } - } - else if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User)) - { - e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); - } - - if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) - { - e("Due time must be in the future.", nameof(command.DueTime)); - } - }); - } - - public static void CanDelete(ISchemaEntity schema, DeleteContent command) - { - Guard.NotNull(command, nameof(command)); - - if (schema.SchemaDef.IsSingleton) - { - throw new DomainException("Singleton content cannot be deleted."); - } - } - - private static void ValidateData(ContentDataCommand command, AddValidation e) - { - if (command.Data == null) - { - e(Not.Defined("Data"), nameof(command.Data)); - } - } - - private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow) - { - if (!await contentWorkflow.CanUpdateAsync(content)) - { - throw new DomainException($"The workflow does not allow updates at status {content.Status}"); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs deleted file mode 100644 index 1a7e53424..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public interface IContentEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - NamedId AppId { get; } - - NamedId SchemaId { get; } - - Status Status { get; } - - ScheduleJob ScheduleJob { get; } - - NamedContentData Data { get; } - - NamedContentData DataDraft { get; } - - bool IsPending { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs deleted file mode 100644 index b8e45f2eb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies - { - bool CanUpdate { get; } - - string StatusColor { get; } - - string SchemaName { get; } - - string SchemaDisplayName { get; } - - RootField[] ReferenceFields { get; } - - StatusInfo[] Nexts { get; } - - NamedContentData ReferenceData { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs deleted file mode 100644 index b24333d95..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ /dev/null @@ -1,377 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.ExtractReferenceIds; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class ContentEnricher : IContentEnricher - { - private const string DefaultColor = StatusColors.Draft; - private static readonly ILookup EmptyContents = Enumerable.Empty().ToLookup(x => x.Id); - private static readonly ILookup EmptyAssets = Enumerable.Empty().ToLookup(x => x.Id); - private readonly IAssetQueryService assetQuery; - private readonly IAssetUrlGenerator assetUrlGenerator; - private readonly Lazy contentQuery; - private readonly IContentWorkflow contentWorkflow; - - private IContentQueryService ContentQuery - { - get { return contentQuery.Value; } - } - - public ContentEnricher(IAssetQueryService assetQuery, IAssetUrlGenerator assetUrlGenerator, Lazy contentQuery, IContentWorkflow contentWorkflow) - { - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); - - this.assetQuery = assetQuery; - this.assetUrlGenerator = assetUrlGenerator; - this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; - } - - public async Task EnrichAsync(IContentEntity content, Context context) - { - Guard.NotNull(content, nameof(content)); - - var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), context); - - return enriched[0]; - } - - public async Task> EnrichAsync(IEnumerable contents, Context context) - { - Guard.NotNull(contents, nameof(contents)); - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var results = new List(); - - if (contents.Any()) - { - var appVersion = context.App.Version; - - var cache = new Dictionary<(Guid, Status), StatusInfo>(); - - foreach (var content in contents) - { - var result = SimpleMapper.Map(content, new ContentEntity()); - - await EnrichColorAsync(content, result, cache); - - if (ShouldEnrichWithStatuses(context)) - { - await EnrichNextsAsync(content, result, context); - await EnrichCanUpdateAsync(content, result); - } - - results.Add(result); - } - - foreach (var group in results.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - foreach (var content in group) - { - content.CacheDependencies = new HashSet - { - schema.Id, - schema.Version - }; - } - - if (ShouldEnrichWithSchema(context)) - { - var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); - - var schemaName = schema.SchemaDef.Name; - var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); - - foreach (var content in group) - { - content.ReferenceFields = referenceFields; - content.SchemaName = schemaName; - content.SchemaDisplayName = schemaDisplayName; - } - } - } - - if (ShouldEnrich(context)) - { - await EnrichReferencesAsync(context, results); - await EnrichAssetsAsync(context, results); - } - } - - return results; - } - } - - private async Task EnrichAssetsAsync(Context context, List contents) - { - var ids = new HashSet(); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - AddAssetIds(ids, schema, group); - } - - var assets = await GetAssetsAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - ResolveAssets(schema, group, assets); - } - } - - private async Task EnrichReferencesAsync(Context context, List contents) - { - var ids = new HashSet(); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - AddReferenceIds(ids, schema, group); - } - - var references = await GetReferencesAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - await ResolveReferencesAsync(context, schema, group, references); - } - } - - private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable contents, ILookup references) - { - var formatted = new Dictionary(); - - foreach (var field in schema.SchemaDef.ResolvingReferences()) - { - foreach (var content in contents) - { - if (content.ReferenceData == null) - { - content.ReferenceData = new NamedContentData(); - } - - var fieldReference = content.ReferenceData.GetOrAddNew(field.Name); - - try - { - if (content.DataDraft.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var referencedContents = - field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) - .Select(x => references[x]) - .SelectMany(x => x) - .ToList(); - - if (referencedContents.Count == 1) - { - var reference = referencedContents[0]; - - var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString()); - - content.CacheDependencies.Add(referencedSchema.Id); - content.CacheDependencies.Add(referencedSchema.Version); - content.CacheDependencies.Add(reference.Id); - content.CacheDependencies.Add(reference.Version); - - var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); - - fieldReference.AddJsonValue(partitionValue.Key, value); - } - else if (referencedContents.Count > 1) - { - var value = CreateFallback(context, referencedContents); - - fieldReference.AddJsonValue(partitionValue.Key, value); - } - } - } - } - catch (DomainObjectNotFoundException) - { - continue; - } - } - } - } - - private void ResolveAssets(ISchemaEntity schema, IGrouping contents, ILookup assets) - { - foreach (var field in schema.SchemaDef.ResolvingAssets()) - { - foreach (var content in contents) - { - if (content.ReferenceData == null) - { - content.ReferenceData = new NamedContentData(); - } - - var fieldReference = content.ReferenceData.GetOrAddNew(field.Name); - - if (content.DataDraft.TryGetValue(field.Name, out var fieldData)) - { - foreach (var partitionValue in fieldData) - { - var referencedImage = - field.GetReferencedIds(partitionValue.Value, Ids.ContentOnly) - .Select(x => assets[x]) - .SelectMany(x => x) - .FirstOrDefault(x => x.IsImage); - - if (referencedImage != null) - { - var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); - - content.CacheDependencies.Add(referencedImage.Id); - content.CacheDependencies.Add(referencedImage.Version); - - fieldReference.AddJsonValue(partitionValue.Key, JsonValue.Create(url)); - } - } - } - } - } - } - - private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) - { - return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); - } - - private static JsonObject CreateFallback(Context context, List referencedContents) - { - var text = $"{referencedContents.Count} Reference(s)"; - - var value = JsonValue.Object(); - - foreach (var language in context.App.LanguagesConfig) - { - value.Add(language.Key, text); - } - - return value; - } - - private void AddReferenceIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) - { - foreach (var content in contents) - { - ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly)); - } - } - - private void AddAssetIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) - { - foreach (var content in contents) - { - ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly)); - } - } - - private async Task> GetReferencesAsync(Context context, HashSet ids) - { - if (ids.Count == 0) - { - return EmptyContents; - } - - var references = await ContentQuery.QueryAsync(context.Clone().WithNoContentEnrichment(true), ids.ToList()); - - return references.ToLookup(x => x.Id); - } - - private async Task> GetAssetsAsync(Context context, HashSet ids) - { - if (ids.Count == 0) - { - return EmptyAssets; - } - - var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), Q.Empty.WithIds(ids)); - - return assets.ToLookup(x => x.Id); - } - - private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result) - { - result.CanUpdate = await contentWorkflow.CanUpdateAsync(content); - } - - private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context) - { - result.Nexts = await contentWorkflow.GetNextsAsync(content, context.User); - } - - private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) - { - result.StatusColor = await GetColorAsync(content, cache); - } - - private async Task GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) - { - if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) - { - info = await contentWorkflow.GetInfoAsync(content); - - if (info == null) - { - info = new StatusInfo(content.Status, DefaultColor); - } - - cache[(content.SchemaId.Id, content.Status)] = info; - } - - return info.Color; - } - - private static bool ShouldEnrichWithSchema(Context context) - { - return context.IsFrontendClient; - } - - private static bool ShouldEnrichWithStatuses(Context context) - { - return context.IsFrontendClient || context.IsResolveFlow(); - } - - private static bool ShouldEnrich(Context context) - { - return context.IsFrontendClient && !context.IsNoEnrichment(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs deleted file mode 100644 index 5abb7f687..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class ContentLoader : IContentLoader - { - private readonly IGrainFactory grainFactory; - - public ContentLoader(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task GetAsync(Guid id, long version) - { - using (Profiler.TraceMethod()) - { - var grain = grainFactory.GetGrain(id); - - var content = await grain.GetStateAsync(version); - - if (content.Value == null || (version > EtagVersion.Any && content.Value.Version != version)) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); - } - - return content.Value; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs deleted file mode 100644 index 6de9304c2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ /dev/null @@ -1,205 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Microsoft.OData; -using Microsoft.OData.Edm; -using NJsonSchema; -using Squidex.Domain.Apps.Core.GenerateEdmSchema; -using Squidex.Domain.Apps.Core.GenerateJsonSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Queries.OData; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentQueryParser : CachingProviderBase - { - private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); - private readonly IJsonSerializer jsonSerializer; - private readonly ContentOptions options; - - public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions options) - : base(cache) - { - this.jsonSerializer = jsonSerializer; - this.options = options.Value; - } - - public virtual ClrQuery ParseQuery(Context context, ISchemaEntity schema, Q q) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(schema, nameof(schema)); - - using (Profiler.TraceMethod()) - { - var result = new ClrQuery(); - - if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) - { - result = ParseJson(context, schema, q.JsonQuery); - } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) - { - result = ParseOData(context, schema, q.ODataQuery); - } - - if (result.Sort.Count == 0) - { - result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); - } - - if (result.Take == long.MaxValue) - { - result.Take = options.DefaultPageSize; - } - else if (result.Take > options.MaxResults) - { - result.Take = options.MaxResults; - } - - return result; - } - } - - private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) - { - var jsonSchema = BuildJsonSchema(context, schema); - - return jsonSchema.Parse(json, jsonSerializer); - } - - private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) - { - try - { - var model = BuildEdmModel(context, schema); - - return model.ParseQuery(odata).ToQuery(); - } - catch (NotSupportedException) - { - throw new ValidationException("OData operation is not supported."); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - } - - private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) - { - var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); - - var result = Cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheTime; - - return BuildJsonSchema(schema.SchemaDef, context.App, context.IsFrontendClient); - }); - - return result; - } - - private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) - { - var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); - - var result = Cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheTime; - - return BuildEdmModel(schema.SchemaDef, context.App, context.IsFrontendClient); - }); - - return result; - } - - private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields) - { - var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); - - return new ContentSchemaBuilder().CreateContentSchema(schema, dataSchema); - } - - private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) - { - var model = new EdmModel(); - - var pascalAppName = app.Name.ToPascalCase(); - var pascalSchemaName = schema.Name.ToPascalCase(); - - var typeFactory = new EdmTypeFactory(name => - { - var finalName = pascalSchemaName; - - if (!string.IsNullOrWhiteSpace(name)) - { - finalName += "."; - finalName += name; - } - - var result = model.SchemaElements.OfType().FirstOrDefault(x => x.Name == finalName); - - if (result != null) - { - return (result, false); - } - - result = new EdmComplexType(pascalAppName, finalName); - - model.AddElement(result); - - return (result, true); - }); - - var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); - - var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); - entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); - entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); - - var container = new EdmEntityContainer("Squidex", "Container"); - - container.AddEntitySet("ContentSet", entityType); - - model.AddElement(container); - model.AddElement(schemaType); - model.AddElement(entityType); - - return model; - } - - private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) - { - return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; - } - - private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) - { - return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs deleted file mode 100644 index aa43adaa1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ /dev/null @@ -1,341 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared; - -#pragma warning disable RECS0147 - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class ContentQueryService : IContentQueryService - { - private static readonly Status[] StatusPublishedOnly = { Status.Published }; - private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); - private readonly IAppProvider appProvider; - private readonly IAssetUrlGenerator assetUrlGenerator; - private readonly IContentEnricher contentEnricher; - private readonly IContentRepository contentRepository; - private readonly IContentLoader contentVersionLoader; - private readonly IScriptEngine scriptEngine; - private readonly ContentQueryParser queryParser; - - public ContentQueryService( - IAppProvider appProvider, - IAssetUrlGenerator assetUrlGenerator, - IContentEnricher contentEnricher, - IContentRepository contentRepository, - IContentLoader contentVersionLoader, - IScriptEngine scriptEngine, - ContentQueryParser queryParser) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); - Guard.NotNull(contentEnricher, nameof(contentEnricher)); - Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader)); - Guard.NotNull(queryParser, nameof(queryParser)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - - this.appProvider = appProvider; - this.assetUrlGenerator = assetUrlGenerator; - this.contentEnricher = contentEnricher; - this.contentRepository = contentRepository; - this.contentVersionLoader = contentVersionLoader; - this.queryParser = queryParser; - this.scriptEngine = scriptEngine; - this.queryParser = queryParser; - } - - public async Task FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = -1) - { - Guard.NotNull(context, nameof(context)); - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - - CheckPermission(context, schema); - - using (Profiler.TraceMethod()) - { - IContentEntity content; - - if (version > EtagVersion.Empty) - { - content = await FindByVersionAsync(id, version); - } - else - { - content = await FindCoreAsync(context, id, schema); - } - - if (content == null || content.SchemaId.Id != schema.Id) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); - } - - return await TransformAsync(context, schema, content); - } - } - - public async Task> QueryAsync(Context context, string schemaIdOrName, Q query) - { - Guard.NotNull(context, nameof(context)); - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - - CheckPermission(context, schema); - - using (Profiler.TraceMethod()) - { - IResultList contents; - - if (query.Ids != null && query.Ids.Count > 0) - { - contents = await QueryByIdsAsync(context, schema, query); - } - else - { - contents = await QueryByQueryAsync(context, schema, query); - } - - return await TransformAsync(context, schema, contents); - } - } - - public async Task> QueryAsync(Context context, IReadOnlyList ids) - { - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - if (ids == null || ids.Count == 0) - { - return EmptyContents; - } - - var results = new List(); - - var contents = await QueryCoreAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.Schema.Id)) - { - var schema = group.First().Schema; - - if (HasPermission(context, schema)) - { - var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content)); - - results.AddRange(enriched); - } - } - - return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); - } - } - - private async Task> TransformAsync(Context context, ISchemaEntity schema, IResultList contents) - { - var transformed = await TransformCoreAsync(context, schema, contents); - - return ResultList.Create(contents.Total, transformed); - } - - private async Task TransformAsync(Context context, ISchemaEntity schema, IContentEntity content) - { - var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1)); - - return transformed[0]; - } - - private async Task> TransformCoreAsync(Context context, ISchemaEntity schema, IEnumerable contents) - { - using (Profiler.TraceMethod()) - { - var results = new List(); - - var converters = GenerateConverters(context).ToArray(); - - var scriptText = schema.SchemaDef.Scripts.Query; - var scripting = !string.IsNullOrWhiteSpace(scriptText); - - var enriched = await contentEnricher.EnrichAsync(contents, context); - - foreach (var content in enriched) - { - var result = SimpleMapper.Map(content, new ContentEntity()); - - if (result.Data != null) - { - if (!context.IsFrontendClient && scripting) - { - var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; - - result.Data = scriptEngine.Transform(ctx, scriptText); - } - - result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); - } - - if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient)) - { - result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters); - } - else - { - result.DataDraft = null; - } - - results.Add(result); - } - - return results; - } - } - - private IEnumerable GenerateConverters(Context context) - { - if (!context.IsFrontendClient) - { - yield return FieldConverters.ExcludeHidden(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); - } - - yield return FieldConverters.ExcludeChangedTypes(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); - - yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); - yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); - - if (!context.IsFrontendClient) - { - yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig); - - var languages = context.Languages(); - - if (languages.Any()) - { - yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); - } - - var assetUrls = context.AssetUrls(); - - if (assetUrls.Any()) - { - yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator); - } - } - } - - public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) - { - ISchemaEntity schema = null; - - if (Guid.TryParse(schemaIdOrName, out var id)) - { - schema = await appProvider.GetSchemaAsync(context.App.Id, id); - } - - if (schema == null) - { - schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName); - } - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); - } - - return schema; - } - - private static void CheckPermission(Context context, params ISchemaEntity[] schemas) - { - foreach (var schema in schemas) - { - if (!HasPermission(context, schema)) - { - throw new DomainForbiddenException("You do not have permission for this schema."); - } - } - } - - private static bool HasPermission(Context context, ISchemaEntity schema) - { - var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); - - return context.Permissions.Allows(permission); - } - - private static Status[] GetStatus(Context context) - { - if (context.IsFrontendClient || context.IsUnpublished()) - { - return null; - } - else - { - return StatusPublishedOnly; - } - } - - private async Task> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) - { - var parsedQuery = queryParser.ParseQuery(context, schema, query); - - return await QueryCoreAsync(context, schema, parsedQuery); - } - - private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) - { - var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); - - return contents.SortSet(x => x.Id, query.Ids); - } - - private Task> QueryCoreAsync(Context context, IReadOnlyList ids) - { - return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet(ids), WithDraft(context)); - } - - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query) - { - return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context)); - } - - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) - { - return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context)); - } - - private Task FindCoreAsync(Context context, Guid id, ISchemaEntity schema) - { - return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context)); - } - - private Task FindByVersionAsync(Guid id, long version) - { - return contentVersionLoader.GetAsync(id, version); - } - - private static bool WithDraft(Context context) - { - return context.IsUnpublished() || context.IsFrontendClient; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs deleted file mode 100644 index e0f812403..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public sealed class FilterTagTransformer : TransformVisitor - { - private readonly ITagService tagService; - private readonly ISchemaEntity schema; - private readonly Guid appId; - - private FilterTagTransformer(Guid appId, ISchemaEntity schema, ITagService tagService) - { - this.appId = appId; - this.schema = schema; - this.tagService = tagService; - } - - public static FilterNode Transform(FilterNode nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService) - { - Guard.NotNull(nodeIn, nameof(nodeIn)); - Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(schema, nameof(schema)); - - return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path)) - { - var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result; - - if (tagNames.TryGetValue(stringValue, out var normalized)) - { - return new CompareFilter(nodeIn.Path, nodeIn.Operator, normalized); - } - } - - return nodeIn; - } - - private static bool IsDataPath(IReadOnlyList path) - { - return path.Count == 3 && string.Equals(path[0], nameof(IContentEntity.Data), StringComparison.OrdinalIgnoreCase); - } - - private bool IsTagField(IReadOnlyList path) - { - return schema.SchemaDef.FieldsByName.TryGetValue(path[1], out var field) && IsTagField(field); - } - - private bool IsTagField(IField field) - { - return field is IField tags && tags.Properties.Normalization == TagsFieldNormalization.Schema; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs deleted file mode 100644 index d6a8e6456..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class QueryExecutionContext - { - private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); - private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); - private readonly IContentQueryService contentQuery; - private readonly IAssetQueryService assetQuery; - private readonly Context context; - - public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery) - { - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(context, nameof(context)); - - this.assetQuery = assetQuery; - this.contentQuery = contentQuery; - this.context = context; - } - - public virtual async Task FindAssetAsync(Guid id) - { - var asset = cachedAssets.GetOrDefault(id); - - if (asset == null) - { - asset = await assetQuery.FindAssetAsync(context, id); - - if (asset != null) - { - cachedAssets[asset.Id] = asset; - } - } - - return asset; - } - - public virtual async Task FindContentAsync(Guid schemaId, Guid id) - { - var content = cachedContents.GetOrDefault(id); - - if (content == null) - { - content = await contentQuery.FindContentAsync(context, schemaId.ToString(), id); - - if (content != null) - { - cachedContents[content.Id] = content; - } - } - - return content; - } - - public virtual async Task> QueryAssetsAsync(string query) - { - var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); - - foreach (var asset in assets) - { - cachedAssets[asset.Id] = asset; - } - - return assets; - } - - public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) - { - var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); - - foreach (var content in result) - { - cachedContents[content.Id] = content; - } - - return result; - } - - public virtual async Task> GetReferencedAssetsAsync(ICollection ids) - { - Guard.NotNull(ids, nameof(ids)); - - var notLoadedAssets = new HashSet(ids.Where(id => !cachedAssets.ContainsKey(id))); - - if (notLoadedAssets.Count > 0) - { - var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets)); - - foreach (var asset in assets) - { - cachedAssets[asset.Id] = asset; - } - } - - return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); - } - - public virtual async Task> GetReferencedContentsAsync(ICollection ids) - { - Guard.NotNull(ids, nameof(ids)); - - var notLoadedContents = ids.Where(id => !cachedContents.ContainsKey(id)).ToList(); - - if (notLoadedContents.Count > 0) - { - var result = await contentQuery.QueryAsync(context, notLoadedContents); - - foreach (var content in result) - { - cachedContents[content.Id] = content; - } - } - - return ids.Select(cachedContents.GetOrDefault).Where(x => x != null).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs deleted file mode 100644 index 5b06f2dc4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.Contents.Repositories -{ - public interface IContentRepository - { - Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids, bool includeDraft); - - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids, bool includeDraft); - - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft); - - Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); - - Task> QueryIdsAsync(Guid appId, HashSet ids); - - Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft); - - Task QueryScheduledWithoutDataAsync(Instant now, Func callback); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs deleted file mode 100644 index 5865a221d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable IDE0060 // Remove unused parameter - -namespace Squidex.Domain.Apps.Entities.Contents.State -{ - public class ContentState : DomainObjectState, IContentEntity - { - [DataMember] - public NamedId AppId { get; set; } - - [DataMember] - public NamedId SchemaId { get; set; } - - [DataMember] - public NamedContentData Data { get; set; } - - [DataMember] - public NamedContentData DataDraft { get; set; } - - [DataMember] - public ScheduleJob ScheduleJob { get; set; } - - [DataMember] - public bool IsPending { get; set; } - - [DataMember] - public bool IsDeleted { get; set; } - - [DataMember] - public Status Status { get; set; } - - public void ApplyEvent(IEvent @event) - { - switch (@event) - { - case ContentCreated e: - { - SimpleMapper.Map(e, this); - - UpdateData(null, e.Data, false); - - break; - } - - case ContentChangesPublished _: - { - ScheduleJob = null; - - UpdateData(DataDraft, null, false); - - break; - } - - case ContentStatusChanged e: - { - ScheduleJob = null; - - SimpleMapper.Map(e, this); - - if (e.Status == Status.Published) - { - UpdateData(DataDraft, null, false); - } - - break; - } - - case ContentUpdated e: - { - UpdateData(e.Data, e.Data, false); - - break; - } - - case ContentUpdateProposed e: - { - UpdateData(null, e.Data, true); - - break; - } - - case ContentChangesDiscarded _: - { - UpdateData(null, Data, false); - - break; - } - - case ContentSchedulingCancelled _: - { - ScheduleJob = null; - - break; - } - - case ContentStatusScheduled e: - { - ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); - - break; - } - - case ContentDeleted _: - { - IsDeleted = true; - - break; - } - } - } - - public override ContentState Apply(Envelope @event) - { - return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); - } - - private void UpdateData(NamedContentData data, NamedContentData dataDraft, bool isPending) - { - if (data != null) - { - Data = data; - } - - if (dataDraft != null) - { - DataDraft = dataDraft; - } - - IsPending = isPending; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs deleted file mode 100644 index e4869b4e3..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed class GrainTextIndexer : ITextIndexer, IEventConsumer - { - private readonly IGrainFactory grainFactory; - - public string Name - { - get { return "TextIndexer"; } - } - - public string EventsFilter - { - get { return "^content-"; } - } - - public GrainTextIndexer(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (@event.Payload is ContentEvent contentEvent) - { - var index = grainFactory.GetGrain(contentEvent.SchemaId.Id); - - var id = contentEvent.ContentId; - - switch (@event.Payload) - { - case ContentDeleted _: - await index.DeleteAsync(id); - break; - case ContentCreated contentCreated: - await index.IndexAsync(Data(id, contentCreated.Data, true)); - break; - case ContentUpdateProposed contentUpdateProposed: - await index.IndexAsync(Data(id, contentUpdateProposed.Data, true)); - break; - case ContentUpdated contentUpdated: - await index.IndexAsync(Data(id, contentUpdated.Data, false)); - break; - case ContentChangesDiscarded _: - await index.CopyAsync(id, false); - break; - case ContentChangesPublished _: - case ContentStatusChanged contentStatusChanged when contentStatusChanged.Status == Status.Published: - await index.CopyAsync(id, true); - break; - } - } - } - - private static J Data(Guid contentId, NamedContentData data, bool onlySelf) - { - return new Update { Id = contentId, Data = data, OnlyDraft = onlySelf }; - } - - public async Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published) - { - if (string.IsNullOrWhiteSpace(queryText)) - { - return null; - } - - var index = grainFactory.GetGrain(schemaId); - - using (Profiler.TraceMethod()) - { - var context = CreateContext(app, scope); - - return await index.SearchAsync(queryText, context); - } - } - - private static SearchContext CreateContext(IAppEntity app, Scope scope) - { - var languages = new HashSet(app.LanguagesConfig.Select(x => x.Key)); - - return new SearchContext { Languages = languages, Scope = scope }; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs deleted file mode 100644 index 4e86a644e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public interface ITextIndexer - { - Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs deleted file mode 100644 index 4a289c91b..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexState.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Lucene.Net.Documents; -using Lucene.Net.Index; -using Lucene.Net.Search; -using Lucene.Net.Util; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - internal sealed class IndexState - { - private const int NotFound = -1; - private const string MetaFor = "_fd"; - private readonly IndexSearcher indexSearcher; - private readonly IndexWriter indexWriter; - private readonly BinaryDocValues binaryValues; - private readonly Dictionary<(Guid, byte), BytesRef> changes = new Dictionary<(Guid, byte), BytesRef>(); - private bool isClosed; - - public IndexState(IndexWriter indexWriter, IndexReader indexReader = null, IndexSearcher indexSearcher = null) - { - this.indexSearcher = indexSearcher; - this.indexWriter = indexWriter; - - if (indexReader != null) - { - binaryValues = MultiDocValues.GetBinaryValues(indexReader, MetaFor); - } - } - - public void Index(Guid id, byte draft, Document document, byte forDraft, byte forPublished) - { - var value = GetValue(forDraft, forPublished); - - document.SetBinaryDocValue(MetaFor, value); - - changes[(id, draft)] = value; - } - - public void Index(Guid id, byte draft, Term term, byte forDraft, byte forPublished) - { - var value = GetValue(forDraft, forPublished); - - indexWriter.UpdateBinaryDocValue(term, MetaFor, value); - - changes[(id, draft)] = value; - } - - public bool HasBeenAdded(Guid id, byte draft, Term term, out int docId) - { - docId = 0; - - if (changes.ContainsKey((id, draft))) - { - return true; - } - - if (indexSearcher != null && !isClosed) - { - var docs = indexSearcher.Search(new TermQuery(term), 1); - - docId = docs?.ScoreDocs.FirstOrDefault()?.Doc ?? NotFound; - - return docId > NotFound; - } - - return false; - } - - public bool TryGet(Guid id, byte draft, int docId, out byte forDraft, out byte forPublished) - { - forDraft = 0; - forPublished = 0; - - if (changes.TryGetValue((id, draft), out var forValue)) - { - forDraft = forValue.Bytes[0]; - forPublished = forValue.Bytes[1]; - - return true; - } - - if (!isClosed && docId != NotFound) - { - forValue = new BytesRef(); - - binaryValues?.Get(docId, forValue); - - if (forValue.Bytes.Length == 2) - { - forDraft = forValue.Bytes[0]; - forPublished = forValue.Bytes[1]; - - changes[(id, draft)] = forValue; - - return true; - } - } - - return false; - } - - public bool TryGet(int docId, out byte forDraft, out byte forPublished) - { - forDraft = 0; - forPublished = 0; - - if (!isClosed && docId != NotFound) - { - var forValue = new BytesRef(); - - binaryValues?.Get(docId, forValue); - - if (forValue.Bytes.Length == 2) - { - forDraft = forValue.Bytes[0]; - forPublished = forValue.Bytes[1]; - - return true; - } - } - - return false; - } - - private static BytesRef GetValue(byte forDraft, byte forPublished) - { - return new BytesRef(new[] { forDraft, forPublished }); - } - - public void CloseReader() - { - isClosed = true; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs deleted file mode 100644 index 8bd41df9c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Lucene.Net.Analysis; -using Lucene.Net.Analysis.Standard; -using Lucene.Net.Util; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed class MultiLanguageAnalyzer : AnalyzerWrapper - { - private readonly StandardAnalyzer fallbackAnalyzer; - private readonly Dictionary analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public MultiLanguageAnalyzer(LuceneVersion version) - : base(PER_FIELD_REUSE_STRATEGY) - { - fallbackAnalyzer = new StandardAnalyzer(version); - - foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) - { - if (typeof(Analyzer).IsAssignableFrom(type)) - { - var language = type.Namespace.Split('.').Last(); - - if (language.Length == 2) - { - try - { - var analyzer = Activator.CreateInstance(type, version); - - analyzers[language] = (Analyzer)analyzer; - } - catch (MissingMethodException) - { - continue; - } - } - } - } - } - - protected override Analyzer GetWrappedAnalyzer(string fieldName) - { - if (fieldName.Length > 0) - { - var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer; - - return analyzer; - } - else - { - return fallbackAnalyzer; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs deleted file mode 100644 index 62fa1a033..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexContent.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Text; -using Lucene.Net.Documents; -using Lucene.Net.Index; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - internal sealed class TextIndexContent - { - private const string MetaId = "_id"; - private const string MetaKey = "_key"; - private readonly IndexWriter indexWriter; - private readonly IndexState indexState; - private readonly Guid id; - - public TextIndexContent(IndexWriter indexWriter, IndexState indexState, Guid id) - { - this.indexWriter = indexWriter; - this.indexState = indexState; - - this.id = id; - } - - public void Delete() - { - indexWriter.DeleteDocuments(new Term(MetaId, id.ToString())); - } - - public static bool TryGetId(int docId, Scope scope, IndexReader reader, IndexState indexState, out Guid result) - { - result = Guid.Empty; - - if (!indexState.TryGet(docId, out var draft, out var published)) - { - return false; - } - - if (scope == Scope.Draft && draft != 1) - { - return false; - } - - if (scope == Scope.Published && published != 1) - { - return false; - } - - var document = reader.Document(docId); - - var idString = document.Get(MetaId); - - if (!Guid.TryParse(idString, out result)) - { - return false; - } - - return true; - } - - public void Index(NamedContentData data, bool onlyDraft) - { - var converted = CreateDocument(data); - - Upsert(converted, 1, 1, 0); - - var isPublishDocumentAdded = IsAdded(0, out var docId); - var isPublishForPublished = IsForPublished(0, docId); - - if (!onlyDraft && isPublishDocumentAdded && isPublishForPublished) - { - Upsert(converted, 0, 0, 1); - } - else if (!onlyDraft || !isPublishDocumentAdded) - { - Upsert(converted, 0, 0, 0); - } - else - { - UpdateFor(0, 0, isPublishForPublished ? (byte)1 : (byte)0); - } - } - - public void Copy(bool fromDraft) - { - if (fromDraft) - { - UpdateFor(1, 1, 0); - UpdateFor(0, 0, 1); - } - else - { - UpdateFor(1, 0, 0); - UpdateFor(0, 1, 1); - } - } - - private static Document CreateDocument(NamedContentData data) - { - var languages = new Dictionary(); - - void AppendText(string language, string text) - { - if (!string.IsNullOrWhiteSpace(text)) - { - var sb = languages.GetOrAddNew(language); - - if (sb.Length > 0) - { - sb.Append(" "); - } - - sb.Append(text); - } - } - - foreach (var field in data) - { - foreach (var fieldValue in field.Value) - { - var appendText = new Action(text => AppendText(fieldValue.Key, text)); - - AppendJsonText(fieldValue.Value, appendText); - } - } - - var document = new Document(); - - foreach (var field in languages) - { - document.AddTextField(field.Key, field.Value.ToString(), Field.Store.NO); - } - - return document; - } - - private void UpdateFor(byte draft, byte forDraft, byte forPublished) - { - var term = new Term(MetaKey, BuildKey(draft)); - - indexState.Index(id, draft, term, forDraft, forPublished); - } - - private void Upsert(Document document, byte draft, byte forDraft, byte forPublished) - { - if (document != null) - { - document.RemoveField(MetaId); - document.RemoveField(MetaKey); - - var contentId = id.ToString(); - var contentKey = BuildKey(draft); - - document.AddStringField(MetaId, contentId, Field.Store.YES); - document.AddStringField(MetaKey, contentKey, Field.Store.YES); - - indexState.Index(id, draft, document, forDraft, forPublished); - - indexWriter.UpdateDocument(new Term(MetaKey, contentKey), document); - } - } - - private static void AppendJsonText(IJsonValue value, Action appendText) - { - if (value.Type == JsonValueType.String) - { - appendText(value.ToString()); - } - else if (value is JsonArray array) - { - foreach (var item in array) - { - AppendJsonText(item, appendText); - } - } - else if (value is JsonObject obj) - { - foreach (var item in obj.Values) - { - AppendJsonText(item, appendText); - } - } - } - - private bool IsAdded(byte draft, out int docId) - { - return indexState.HasBeenAdded(id, draft, new Term(MetaKey, BuildKey(draft)), out docId); - } - - private bool IsForPublished(byte draft, int docId) - { - return indexState.TryGet(id, draft, docId, out _, out var p) && p == 1; - } - - private string BuildKey(byte draft) - { - return $"{id}_{draft}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs deleted file mode 100644 index fb1133d9e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs +++ /dev/null @@ -1,259 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Lucene.Net.Analysis; -using Lucene.Net.Index; -using Lucene.Net.QueryParsers.Classic; -using Lucene.Net.Search; -using Lucene.Net.Store; -using Lucene.Net.Util; -using Squidex.Domain.Apps.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain - { - private const LuceneVersion Version = LuceneVersion.LUCENE_48; - private const int MaxResults = 2000; - private const int MaxUpdates = 400; - private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(10); - private static readonly MergeScheduler MergeScheduler = new ConcurrentMergeScheduler(); - private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version); - private static readonly string[] Invariant = { InvariantPartitioning.Key }; - private readonly SnapshotDeletionPolicy snapshotter = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); - private readonly IAssetStore assetStore; - private IDisposable timer; - private DirectoryInfo directory; - private IndexWriter indexWriter; - private IndexReader indexReader; - private IndexSearcher indexSearcher; - private IndexState indexState; - private QueryParser queryParser; - private HashSet currentLanguages; - private int updates; - - public TextIndexerGrain(IAssetStore assetStore) - { - Guard.NotNull(assetStore, nameof(assetStore)); - - this.assetStore = assetStore; - } - - public override async Task OnDeactivateAsync() - { - await DeactivateAsync(true); - } - - protected override async Task OnActivateAsync(Guid key) - { - directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}")); - - await assetStore.DownloadAsync(directory); - - var config = new IndexWriterConfig(Version, Analyzer) - { - IndexDeletionPolicy = snapshotter, - MergePolicy = new TieredMergePolicy(), - MergeScheduler = MergeScheduler - }; - - indexWriter = new IndexWriter(FSDirectory.Open(directory), config); - - if (indexWriter.NumDocs > 0) - { - OpenReader(); - } - else - { - indexState = new IndexState(indexWriter); - } - } - - public Task IndexAsync(J update) - { - return IndexInternalAsync(update); - } - - private Task IndexInternalAsync(Update update) - { - var content = new TextIndexContent(indexWriter, indexState, update.Id); - - content.Index(update.Data, update.OnlyDraft); - - return TryFlushAsync(); - } - - public Task CopyAsync(Guid id, bool fromDraft) - { - var content = new TextIndexContent(indexWriter, indexState, id); - - content.Copy(fromDraft); - - return TryFlushAsync(); - } - - public Task DeleteAsync(Guid id) - { - var content = new TextIndexContent(indexWriter, indexState, id); - - content.Delete(); - - return TryFlushAsync(); - } - - public Task> SearchAsync(string queryText, SearchContext context) - { - var result = new List(); - - if (!string.IsNullOrWhiteSpace(queryText)) - { - var query = BuildQuery(queryText, context); - - if (indexReader == null && indexWriter.NumDocs > 0) - { - OpenReader(); - } - - if (indexReader != null) - { - var found = new HashSet(); - - var hits = indexSearcher.Search(query, MaxResults).ScoreDocs; - - foreach (var hit in hits) - { - if (TextIndexContent.TryGetId(hit.Doc, context.Scope, indexReader, indexState, out var id)) - { - if (found.Add(id)) - { - result.Add(id); - } - } - } - } - } - - return Task.FromResult(result.ToList()); - } - - private Query BuildQuery(string query, SearchContext context) - { - if (queryParser == null || !currentLanguages.SetEquals(context.Languages)) - { - var fields = context.Languages.Union(Invariant).ToArray(); - - queryParser = new MultiFieldQueryParser(Version, fields, Analyzer); - - currentLanguages = context.Languages; - } - - try - { - return queryParser.Parse(query); - } - catch (ParseException ex) - { - throw new ValidationException(ex.Message); - } - } - - private async Task TryFlushAsync() - { - timer?.Dispose(); - - updates++; - - if (updates >= MaxUpdates) - { - await FlushAsync(); - - return true; - } - else - { - CleanReader(); - - try - { - timer = RegisterTimer(_ => FlushAsync(), null, CommitDelay, CommitDelay); - } - catch (InvalidOperationException) - { - return false; - } - } - - return false; - } - - public async Task FlushAsync() - { - if (updates > 0 && indexWriter != null) - { - indexWriter.Commit(); - indexWriter.Flush(true, true); - - CleanReader(); - - var commit = snapshotter.Snapshot(); - try - { - await assetStore.UploadDirectoryAsync(directory, commit); - } - finally - { - snapshotter.Release(commit); - } - - updates = 0; - } - } - - public async Task DeactivateAsync(bool deleteFolder = false) - { - await FlushAsync(); - - CleanWriter(); - CleanReader(); - - if (deleteFolder && directory.Exists) - { - directory.Delete(true); - } - } - - private void OpenReader() - { - indexReader = indexWriter.GetReader(true); - indexSearcher = new IndexSearcher(indexReader); - indexState = new IndexState(indexWriter, indexReader, indexSearcher); - } - - private void CleanReader() - { - indexReader?.Dispose(); - indexReader = null; - indexSearcher = null; - indexState?.CloseReader(); - } - - private void CleanWriter() - { - indexWriter?.Dispose(); - indexWriter = null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Context.cs b/src/Squidex.Domain.Apps.Entities/Context.cs deleted file mode 100644 index 721b12a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Context.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; -using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet; - -namespace Squidex.Domain.Apps.Entities -{ - public sealed class Context - { - public IDictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public IAppEntity App { get; set; } - - public ClaimsPrincipal User { get; } - - public ClaimsPermissions Permissions { get; private set; } = ClaimsPermissions.Empty; - - public bool IsFrontendClient { get; private set; } - - public Context(ClaimsPrincipal user) - { - Guard.NotNull(user, nameof(user)); - - User = user; - - UpdatePermissions(); - } - - public Context(ClaimsPrincipal user, IAppEntity app) - : this(user) - { - App = app; - } - - public static Context Anonymous() - { - return new Context(new ClaimsPrincipal()); - } - - public void UpdatePermissions() - { - Permissions = User.Permissions(); - - IsFrontendClient = User.IsInClient(DefaultClients.Frontend); - } - - public Context Clone() - { - var clone = new Context(User, App); - - foreach (var kvp in Headers) - { - clone.Headers[kvp.Key] = kvp.Value; - } - - return clone; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs deleted file mode 100644 index 8c8d5dfbf..000000000 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities -{ - public static class EntityMapper - { - public static T Update(this T entity, Envelope envelope, Action updater = null) where T : IEntity - { - var @event = (SquidexEvent)envelope.Payload; - - var headers = envelope.Headers; - - SetId(entity, headers); - SetCreated(entity, headers); - SetCreatedBy(entity, @event); - SetLastModified(entity, headers); - SetLastModifiedBy(entity, @event); - SetVersion(entity, headers); - - updater?.Invoke(@event, entity); - - return entity; - } - - private static void SetId(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty) - { - updateable.Id = headers.AggregateId(); - } - } - - private static void SetVersion(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntityWithVersion updateable) - { - updateable.Version = headers.EventStreamNumber(); - } - } - - private static void SetCreated(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntity updateable && updateable.Created == default) - { - updateable.Created = headers.Timestamp(); - } - } - - private static void SetCreatedBy(IEntity entity, SquidexEvent @event) - { - if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) - { - withCreatedBy.CreatedBy = @event.Actor; - } - } - - private static void SetLastModified(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntity updateable) - { - updateable.LastModified = headers.Timestamp(); - } - } - - private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event) - { - if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) - { - withModifiedBy.LastModifiedBy = @event.Actor; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs deleted file mode 100644 index 60dec6de4..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class HistoryEvent - { - public Guid Id { get; set; } = Guid.NewGuid(); - - public Guid AppId { get; set; } - - public Instant Created { get; set; } - - public RefToken Actor { get; set; } - - public long Version { get; set; } - - public string Channel { get; set; } - - public string Message { get; set; } - - public Dictionary Parameters { get; set; } = new Dictionary(); - - public HistoryEvent() - { - } - - public HistoryEvent(string channel, string message) - { - Guard.NotNullOrEmpty(channel, nameof(channel)); - Guard.NotNullOrEmpty(message, nameof(message)); - - Channel = channel; - - Message = message; - } - - public HistoryEvent Param(string key, T value) - { - if (!Equals(value, default(T))) - { - Parameters[key] = value.ToString(); - } - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs deleted file mode 100644 index 9e873e694..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.History -{ - public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator - { - private readonly Dictionary texts = new Dictionary(); - private readonly TypeNameRegistry typeNameRegistry; - - public IReadOnlyDictionary Texts - { - get { return texts; } - } - - protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.typeNameRegistry = typeNameRegistry; - } - - protected void AddEventMessage(string message) where TEvent : IEvent - { - Guard.NotNullOrEmpty(message, nameof(message)); - - texts[typeNameRegistry.GetName()] = message; - } - - protected bool HasEventText(IEvent @event) - { - var message = typeNameRegistry.GetName(@event.GetType()); - - return texts.ContainsKey(message); - } - - protected HistoryEvent ForEvent(IEvent @event, string channel) - { - var message = typeNameRegistry.GetName(@event.GetType()); - - return new HistoryEvent(channel, message); - } - - public Task CreateEventAsync(Envelope @event) - { - if (HasEventText(@event.Payload)) - { - return CreateEventCoreAsync(@event); - } - - return Task.FromResult(null); - } - - protected abstract Task CreateEventCoreAsync(Envelope @event); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs deleted file mode 100644 index e0f8e00c2..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class HistoryService : IHistoryService, IEventConsumer - { - private readonly Dictionary texts = new Dictionary(); - private readonly List creators; - private readonly IHistoryEventRepository repository; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return ".*"; } - } - - public HistoryService(IHistoryEventRepository repository, IEnumerable creators) - { - Guard.NotNull(repository, nameof(repository)); - Guard.NotNull(creators, nameof(creators)); - - this.creators = creators.ToList(); - - foreach (var creator in this.creators) - { - foreach (var text in creator.Texts) - { - texts[text.Key] = text.Value; - } - } - - this.repository = repository; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return repository.ClearAsync(); - } - - public async Task On(Envelope @event) - { - foreach (var creator in creators) - { - var historyEvent = await creator.CreateEventAsync(@event); - - if (historyEvent != null) - { - var appEvent = (AppEvent)@event.Payload; - - historyEvent.Actor = appEvent.Actor; - historyEvent.AppId = appEvent.AppId.Id; - historyEvent.Created = @event.Headers.Timestamp(); - historyEvent.Version = @event.Headers.EventStreamNumber(); - - await repository.InsertAsync(historyEvent); - } - } - } - - public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) - { - var items = await repository.QueryByChannelAsync(appId, channelPrefix, count); - - return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs deleted file mode 100644 index 5b15f92d9..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.History -{ - public interface IHistoryEventsCreator - { - IReadOnlyDictionary Texts { get; } - - Task CreateEventAsync(Envelope @event); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs deleted file mode 100644 index 3a26e2814..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailEventConsumer.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.History.Notifications -{ - public sealed class NotificationEmailEventConsumer : IEventConsumer - { - private static readonly Duration MaxAge = Duration.FromDays(2); - private readonly INotificationEmailSender emailSender; - private readonly IUserResolver userResolver; - private readonly ISemanticLog log; - - public string Name - { - get { return "NotificationEmailSender"; } - } - - public string EventsFilter - { - get { return "^app-"; } - } - - public NotificationEmailEventConsumer(INotificationEmailSender emailSender, IUserResolver userResolver, ISemanticLog log) - { - Guard.NotNull(emailSender, nameof(emailSender)); - Guard.NotNull(userResolver, nameof(userResolver)); - Guard.NotNull(log, nameof(log)); - - this.emailSender = emailSender; - this.userResolver = userResolver; - - this.log = log; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (!emailSender.IsActive) - { - return; - } - - if (@event.Headers.EventStreamNumber() <= 1) - { - return; - } - - var now = SystemClock.Instance.GetCurrentInstant(); - - var timestamp = @event.Headers.Timestamp(); - - if (now - timestamp > MaxAge) - { - return; - } - - if (@event.Payload is AppContributorAssigned appContributorAssigned) - { - if (!appContributorAssigned.Actor.IsSubject || !appContributorAssigned.IsAdded) - { - return; - } - - var assignerId = appContributorAssigned.Actor.Identifier; - var assigneeId = appContributorAssigned.ContributorId; - - var assigner = await userResolver.FindByIdOrEmailAsync(assignerId); - - if (assigner == null) - { - LogWarning($"Assigner {assignerId} not found"); - return; - } - - var assignee = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.ContributorId); - - if (assignee == null) - { - LogWarning($"Assignee {assigneeId} not found"); - return; - } - - var appName = appContributorAssigned.AppId.Name; - - var isCreated = appContributorAssigned.IsCreated; - - await emailSender.SendContributorEmailAsync(assigner, assignee, appName, isCreated); - } - } - - private void LogWarning(string reason) - { - log.LogWarning(w => w - .WriteProperty("action", "InviteUser") - .WriteProperty("status", "Failed") - .WriteProperty("reason", reason)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs b/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs deleted file mode 100644 index 8ee529484..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/Notifications/NotificationEmailSender.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Email; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.History.Notifications -{ - public sealed class NotificationEmailSender : INotificationEmailSender - { - private readonly IEmailSender emailSender; - private readonly IEmailUrlGenerator emailUrlGenerator; - private readonly ISemanticLog log; - private readonly NotificationEmailTextOptions texts; - - public bool IsActive - { - get { return true; } - } - - public NotificationEmailSender( - IOptions texts, - IEmailSender emailSender, - IEmailUrlGenerator emailUrlGenerator, - ISemanticLog log) - { - Guard.NotNull(texts, nameof(texts)); - Guard.NotNull(emailSender, nameof(emailSender)); - Guard.NotNull(emailUrlGenerator, nameof(emailUrlGenerator)); - Guard.NotNull(log, nameof(log)); - - this.texts = texts.Value; - this.emailSender = emailSender; - this.emailUrlGenerator = emailUrlGenerator; - this.log = log; - } - - public Task SendContributorEmailAsync(IUser assigner, IUser assignee, string appName, bool isCreated) - { - Guard.NotNull(assigner, nameof(assigner)); - Guard.NotNull(assignee, nameof(assignee)); - Guard.NotNull(appName, nameof(appName)); - - if (assignee.HasConsent()) - { - return SendEmailAsync(texts.ExistingUserSubject, texts.ExistingUserBody, assigner, assignee, appName); - } - else - { - return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); - } - } - - private async Task SendEmailAsync(string emailSubj, string emailBody, IUser assigner, IUser assignee, string appName) - { - if (string.IsNullOrWhiteSpace(emailBody)) - { - LogWarning("No email subject configured for new users"); - return; - } - - if (string.IsNullOrWhiteSpace(emailSubj)) - { - LogWarning("No email body configured for new users"); - return; - } - - var appUrl = emailUrlGenerator.GenerateUIUrl(); - - emailSubj = Format(emailSubj, assigner, assignee, appUrl, appName); - emailBody = Format(emailBody, assigner, assignee, appUrl, appName); - - await emailSender.SendAsync(assignee.Email, emailSubj, emailBody); - } - - private void LogWarning(string reason) - { - log.LogWarning(w => w - .WriteProperty("action", "InviteUser") - .WriteProperty("status", "Failed") - .WriteProperty("reason", reason)); - } - - private static string Format(string text, IUser assigner, IUser assignee, string uiUrl, string appName) - { - text = text.Replace("$APP_NAME", appName); - - if (assigner != null) - { - text = text.Replace("$ASSIGNER_EMAIL", assigner.Email); - text = text.Replace("$ASSIGNER_NAME", assigner.DisplayName()); - } - - if (assignee != null) - { - text = text.Replace("$ASSIGNEE_EMAIL", assignee.Email); - text = text.Replace("$ASSIGNEE_NAME", assignee.DisplayName()); - } - - text = text.Replace("$UI_URL", uiUrl); - - return text; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs deleted file mode 100644 index 7e5c018b3..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class ParsedHistoryEvent - { - private readonly HistoryEvent item; - private readonly Lazy message; - - public Guid Id - { - get { return item.Id; } - } - - public Instant Created - { - get { return item.Created; } - } - - public RefToken Actor - { - get { return item.Actor; } - } - - public long Version - { - get { return item.Version; } - } - - public string Channel - { - get { return item.Channel; } - } - - public string Message - { - get { return message.Value; } - } - - public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary texts) - { - this.item = item; - - message = new Lazy(() => - { - if (texts.TryGetValue(item.Message, out var result)) - { - foreach (var kvp in item.Parameters) - { - result = result.Replace("[" + kvp.Key + "]", kvp.Value); - } - - return result; - } - - return null; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs deleted file mode 100644 index 8fa6200dc..000000000 --- a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Entities -{ - public interface IAppProvider - { - Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id); - - Task GetAppAsync(Guid appId); - - Task GetAppAsync(string appName); - - Task> GetUserAppsAsync(string userId, PermissionSet permissions); - - Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); - - Task GetSchemaAsync(Guid appId, string name); - - Task> GetSchemasAsync(Guid appId); - - Task> GetRulesAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs deleted file mode 100644 index 1a6b5af96..000000000 --- a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Entities -{ - public interface IEntityWithCacheDependencies - { - HashSet CacheDependencies { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Q.cs b/src/Squidex.Domain.Apps.Entities/Q.cs deleted file mode 100644 index 9fce3c394..000000000 --- a/src/Squidex.Domain.Apps.Entities/Q.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities -{ - public sealed class Q : Cloneable - { - public static readonly Q Empty = new Q(); - - public IReadOnlyList Ids { get; private set; } - - public string ODataQuery { get; private set; } - - public string JsonQuery { get; private set; } - - public Q WithODataQuery(string odataQuery) - { - return Clone(c => c.ODataQuery = odataQuery); - } - - public Q WithJsonQuery(string jsonQuery) - { - return Clone(c => c.JsonQuery = jsonQuery); - } - - public Q WithIds(params Guid[] ids) - { - return Clone(c => c.Ids = ids.ToList()); - } - - public Q WithIds(IEnumerable ids) - { - return Clone(c => c.Ids = ids.ToList()); - } - - public Q WithIds(string ids) - { - if (!string.IsNullOrEmpty(ids)) - { - return Clone(c => - { - var idsList = new List(); - - foreach (var id in ids.Split(',')) - { - if (Guid.TryParse(id, out var guid)) - { - idsList.Add(guid); - } - } - - c.Ids = idsList; - }); - } - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs deleted file mode 100644 index f021bdc16..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class BackupRules : BackupHandler - { - private readonly HashSet ruleIds = new HashSet(); - private readonly IRulesIndex indexForRules; - - public override string Name { get; } = "Rules"; - - public BackupRules(IRulesIndex indexForRules) - { - Guard.NotNull(indexForRules, nameof(indexForRules)); - - this.indexForRules = indexForRules; - } - - public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case RuleCreated ruleCreated: - ruleIds.Add(ruleCreated.RuleId); - break; - case RuleDeleted ruleDeleted: - ruleIds.Remove(ruleDeleted.RuleId); - break; - } - - return TaskHelper.True; - } - - public override Task RestoreAsync(Guid appId, BackupReader reader) - { - return indexForRules.RebuildAsync(appId, ruleIds); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs deleted file mode 100644 index 3ea443623..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Rules.Guards -{ - public static class GuardRule - { - public static Task CanCreate(CreateRule command, IAppProvider appProvider) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create rule.", async e => - { - if (command.Trigger == null) - { - e(Not.Defined("Trigger"), nameof(command.Trigger)); - } - else - { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); - - errors.Foreach(x => x.AddTo(e)); - } - - if (command.Action == null) - { - e(Not.Defined("Action"), nameof(command.Action)); - } - else - { - var errors = command.Action.Validate(); - - errors.Foreach(x => x.AddTo(e)); - } - }); - } - - public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot update rule.", async e => - { - if (command.Trigger == null && command.Action == null && command.Name == null) - { - e(Not.Defined("Either trigger, action or name"), nameof(command.Trigger), nameof(command.Action)); - } - - if (command.Trigger != null) - { - var errors = await RuleTriggerValidator.ValidateAsync(appId, command.Trigger, appProvider); - - errors.Foreach(x => x.AddTo(e)); - } - - if (command.Action != null) - { - var errors = command.Action.Validate(); - - errors.Foreach(x => x.AddTo(e)); - } - - if (command.Name != null && string.Equals(rule.Name, command.Name)) - { - e(Not.New("Rule", "name"), nameof(command.Name)); - } - }); - } - - public static void CanEnable(EnableRule command, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - if (rule.IsEnabled) - { - throw new DomainException("Rule is already enabled."); - } - } - - public static void CanDisable(DisableRule command, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - if (!rule.IsEnabled) - { - throw new DomainException("Rule is already disabled."); - } - } - - public static void CanDelete(DeleteRule command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs deleted file mode 100644 index 4b4f1829d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Rules.Guards -{ - public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> - { - public Func> SchemaProvider { get; } - - public RuleTriggerValidator(Func> schemaProvider) - { - SchemaProvider = schemaProvider; - } - - public static Task> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider) - { - Guard.NotNull(action, nameof(action)); - Guard.NotNull(appProvider, nameof(appProvider)); - - var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); - - return action.Accept(visitor); - } - - public Task> Visit(AssetChangedTriggerV2 trigger) - { - return Task.FromResult(Enumerable.Empty()); - } - - public Task> Visit(ManualTrigger trigger) - { - return Task.FromResult(Enumerable.Empty()); - } - - public Task> Visit(SchemaChangedTrigger trigger) - { - return Task.FromResult(Enumerable.Empty()); - } - - public Task> Visit(UsageTrigger trigger) - { - var errors = new List(); - - if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) - { - errors.Add(new ValidationError(Not.Between("Num days", 1, 30), nameof(trigger.NumDays))); - } - - return Task.FromResult>(errors); - } - - public async Task> Visit(ContentChangedTriggerV2 trigger) - { - var errors = new List(); - - if (trigger.Schemas != null) - { - var tasks = new List>(); - - foreach (var schema in trigger.Schemas) - { - if (schema.SchemaId == Guid.Empty) - { - errors.Add(new ValidationError(Not.Defined("Schema id"), nameof(trigger.Schemas))); - } - else - { - tasks.Add(CheckSchemaAsync(schema)); - } - } - - var checkErrors = await Task.WhenAll(tasks); - - errors.AddRange(checkErrors.Where(x => x != null)); - } - - return errors; - } - - private async Task CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) - { - if (await SchemaProvider(schema.SchemaId) == null) - { - return new ValidationError($"Schema {schema.SchemaId} does not exist.", nameof(ContentChangedTriggerV2.Schemas)); - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs deleted file mode 100644 index aaa2faeb1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public interface IRuleEventEntity : IEntity - { - RuleJob Job { get; } - - Instant? NextAttempt { get; } - - RuleJobResult JobResult { get; } - - RuleResult Result { get; } - - int NumCalls { get; } - - string LastDump { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs deleted file mode 100644 index f57d9154f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public sealed class RulesIndex : ICommandMiddleware, IRulesIndex - { - private readonly IGrainFactory grainFactory; - - public RulesIndex(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public Task RebuildAsync(Guid appId, HashSet rues) - { - return Index(appId).RebuildAsync(rues); - } - - public async Task> GetRulesAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - var ids = await GetRuleIdsAsync(appId); - - var rules = - await Task.WhenAll( - ids.Select(GetRuleAsync)); - - return rules.Where(x => x != null).ToList(); - } - } - - private async Task GetRuleAsync(Guid id) - { - using (Profiler.TraceMethod()) - { - var ruleEntity = await grainFactory.GetGrain(id).GetStateAsync(); - - if (IsFound(ruleEntity.Value)) - { - return ruleEntity.Value; - } - - return null; - } - } - - private async Task> GetRuleIdsAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - return await Index(appId).GetIdsAsync(); - } - } - - public async Task HandleAsync(CommandContext context, Func next) - { - await next(); - - if (context.IsCompleted) - { - switch (context.Command) - { - case CreateRule createRule: - await CreateRuleAsync(createRule); - break; - case DeleteRule deleteRule: - await DeleteRuleAsync(deleteRule); - break; - } - } - } - - private async Task CreateRuleAsync(CreateRule command) - { - await Index(command.AppId.Id).AddAsync(command.RuleId); - } - - private async Task DeleteRuleAsync(DeleteRule command) - { - var id = command.RuleId; - - var rule = await grainFactory.GetGrain(id).GetStateAsync(); - - if (IsFound(rule.Value)) - { - await Index(rule.Value.AppId.Id).RemoveAsync(id); - } - } - - private IRulesByAppIndexGrain Index(Guid appId) - { - return grainFactory.GetGrain(appId); - } - - private static bool IsFound(IRuleEntity rule) - { - return rule.Version > EtagVersion.Empty && !rule.IsDeleted; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs deleted file mode 100644 index 05b3bc5c1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class ManualTriggerHandler : RuleTriggerHandler - { - protected override Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedManualEvent - { - Name = "Manual" - }; - - return Task.FromResult(result); - } - - protected override bool Trigger(EnrichedManualEvent @event, ManualTrigger trigger) - { - return true; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs b/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs deleted file mode 100644 index 01a0024e2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Rules.Queries -{ - public sealed class RuleEnricher : IRuleEnricher - { - private readonly IRuleEventRepository ruleEventRepository; - - public RuleEnricher(IRuleEventRepository ruleEventRepository) - { - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - - this.ruleEventRepository = ruleEventRepository; - } - - public async Task EnrichAsync(IRuleEntity rule, Context context) - { - Guard.NotNull(rule, nameof(rule)); - - var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context); - - return enriched[0]; - } - - public async Task> EnrichAsync(IEnumerable rules, Context context) - { - Guard.NotNull(rules, nameof(rules)); - Guard.NotNull(context, nameof(context)); - - using (Profiler.TraceMethod()) - { - var results = new List(); - - foreach (var rule in rules) - { - var result = SimpleMapper.Map(rule, new RuleEntity()); - - results.Add(result); - } - - foreach (var group in results.GroupBy(x => x.AppId.Id)) - { - var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key); - - foreach (var rule in group) - { - var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); - - if (statistic != null) - { - rule.LastExecuted = statistic.LastExecuted; - rule.NumFailed = statistic.NumFailed; - rule.NumSucceeded = statistic.NumSucceeded; - - rule.CacheDependencies = new HashSet - { - statistic.LastExecuted - }; - } - } - } - - return results; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs deleted file mode 100644 index e6979e9bc..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// 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.Threading; -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Entities.Rules.Repositories -{ - public interface IRuleEventRepository - { - Task EnqueueAsync(RuleJob job, Instant nextAttempt); - - Task EnqueueAsync(Guid id, Instant nextAttempt); - - Task CancelAsync(Guid id); - - Task MarkSentAsync(RuleJob job, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant finished, Instant? nextCall); - - Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default); - - Task CountByAppAsync(Guid appId); - - Task> QueryStatisticsByAppAsync(Guid appId); - - Task> QueryByAppAsync(Guid appId, Guid? ruleId = null, int skip = 0, int take = 20); - - Task FindAsync(Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs deleted file mode 100644 index be304a805..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using NodaTime; -using Orleans; -using Orleans.Runtime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class RuleDequeuerGrain : Grain, IRuleDequeuerGrain, IRemindable - { - private readonly ITargetBlock requestBlock; - private readonly IRuleEventRepository ruleEventRepository; - private readonly RuleService ruleService; - private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); - private readonly IClock clock; - private readonly ISemanticLog log; - - public RuleDequeuerGrain(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) - { - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleService, nameof(ruleService)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - - this.clock = clock; - - this.log = log; - - requestBlock = - new PartitionedActionBlock(HandleAsync, x => x.Job.ExecutionPartition, - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); - } - - public override Task OnActivateAsync() - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => QueryAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); - } - - public override Task OnDeactivateAsync() - { - requestBlock.Complete(); - - return requestBlock.Completion; - } - - public Task ActivateAsync() - { - return TaskHelper.Done; - } - - public async Task QueryAsync() - { - try - { - var now = clock.GetCurrentInstant(); - - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "QueueWebhookEvents") - .WriteProperty("status", "Failed")); - } - } - - public async Task HandleAsync(IRuleEventEntity @event) - { - if (!executing.TryAdd(@event.Id, false)) - { - return; - } - - try - { - var job = @event.Job; - - var (response, elapsed) = await ruleService.InvokeAsync(job.ActionName, job.ActionData); - - var jobInvoke = ComputeJobInvoke(response.Status, @event, job); - var jobResult = ComputeJobResult(response.Status, jobInvoke); - - var now = clock.GetCurrentInstant(); - - await ruleEventRepository.MarkSentAsync(@event.Job, response.Dump, response.Status, jobResult, elapsed, now, jobInvoke); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "SendWebhookEvent") - .WriteProperty("status", "Failed")); - } - finally - { - executing.TryRemove(@event.Id, out _); - } - } - - private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) - { - if (result != RuleResult.Success && !nextCall.HasValue) - { - return RuleJobResult.Failed; - } - else if (result != RuleResult.Success && nextCall.HasValue) - { - return RuleJobResult.Retry; - } - else - { - return RuleJobResult.Success; - } - } - - private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) - { - if (result != RuleResult.Success) - { - switch (@event.NumCalls) - { - case 0: - return job.Created.Plus(Duration.FromMinutes(5)); - case 1: - return job.Created.Plus(Duration.FromHours(1)); - case 2: - return job.Created.Plus(Duration.FromHours(6)); - case 3: - return job.Created.Plus(Duration.FromHours(12)); - } - } - - return null; - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs deleted file mode 100644 index 82564f224..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(10); - private readonly IRuleEventRepository ruleEventRepository; - private readonly IAppProvider appProvider; - private readonly IMemoryCache cache; - private readonly RuleService ruleService; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return ".*"; } - } - - public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, IRuleEventRepository ruleEventRepository, - RuleService ruleService) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(cache, nameof(cache)); - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleService, nameof(ruleService)); - - this.appProvider = appProvider; - - this.cache = cache; - - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task Enqueue(Rule rule, Guid ruleId, Envelope @event) - { - Guard.NotNull(rule, nameof(rule)); - Guard.NotNull(@event, nameof(@event)); - - var job = await ruleService.CreateJobAsync(rule, ruleId, @event); - - if (job != null) - { - await ruleEventRepository.EnqueueAsync(job, job.Created); - } - } - - public async Task On(Envelope @event) - { - if (@event.Payload is AppEvent appEvent) - { - var rules = await GetRulesAsync(appEvent.AppId.Id); - - foreach (var ruleEntity in rules) - { - await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event); - } - } - } - - private Task> GetRulesAsync(Guid appId) - { - return cache.GetOrCreateAsync(appId, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - return appProvider.GetRulesAsync(appId); - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs deleted file mode 100644 index 373f9db95..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleEntity : IEnrichedRuleEntity - { - public Guid Id { get; set; } - - public NamedId AppId { get; set; } - - public NamedId SchemaId { get; set; } - - public long Version { get; set; } - - public Instant Created { get; set; } - - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - - public RefToken LastModifiedBy { get; set; } - - public Rule RuleDef { get; set; } - - public bool IsDeleted { get; set; } - - public int NumSucceeded { get; set; } - - public int NumFailed { get; set; } - - public Instant? LastExecuted { get; set; } - - public HashSet CacheDependencies { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs deleted file mode 100644 index 72993be18..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ /dev/null @@ -1,154 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Rules.Guards; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleGrain : DomainObjectGrain, IRuleGrain - { - private readonly IAppProvider appProvider; - private readonly IRuleEnqueuer ruleEnqueuer; - - public RuleGrain(IStore store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) - : base(store, log) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer)); - - this.appProvider = appProvider; - - this.ruleEnqueuer = ruleEnqueuer; - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case CreateRule createRule: - return CreateReturnAsync(createRule, async c => - { - await GuardRule.CanCreate(c, appProvider); - - Create(c); - - return Snapshot; - }); - case UpdateRule updateRule: - return UpdateReturnAsync(updateRule, async c => - { - await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider, Snapshot.RuleDef); - - Update(c); - - return Snapshot; - }); - case EnableRule enableRule: - return UpdateReturn(enableRule, c => - { - GuardRule.CanEnable(c, Snapshot.RuleDef); - - Enable(c); - - return Snapshot; - }); - case DisableRule disableRule: - return UpdateReturn(disableRule, c => - { - GuardRule.CanDisable(c, Snapshot.RuleDef); - - Disable(c); - - return Snapshot; - }); - case DeleteRule deleteRule: - return Update(deleteRule, c => - { - GuardRule.CanDelete(deleteRule); - - Delete(c); - }); - case TriggerRule triggerRule: - return Trigger(triggerRule); - default: - throw new NotSupportedException(); - } - } - - private async Task Trigger(TriggerRule command) - { - var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId }); - - await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); - - return null; - } - - public void Create(CreateRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); - } - - public void Update(UpdateRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); - } - - public void Enable(EnableRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); - } - - public void Disable(DisableRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); - } - - public void Delete(DeleteRule command) - { - RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Rule has already been deleted."); - } - } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs deleted file mode 100644 index 9cd7202b7..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware - { - private readonly IUsageTrackerGrain usageTrackerGrain; - - public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - switch (context.Command) - { - case DeleteRule deleteRule: - await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); - break; - case CreateRule createRule: - { - if (createRule.Trigger is UsageTrigger usage) - { - await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); - } - - break; - } - - case UpdateRule ruleUpdated: - { - if (ruleUpdated.Trigger is UsageTrigger usage) - { - await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); - } - - break; - } - } - - await next(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs deleted file mode 100644 index 0f7084352..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Orleans; -using Orleans.Concurrency; -using Orleans.Runtime; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.UsageTracking; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - [Reentrant] - public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain - { - private readonly IGrainState state; - private readonly IUsageTracker usageTracker; - - public sealed class Target - { - public NamedId AppId { get; set; } - - public int Limits { get; set; } - - public int? NumDays { get; set; } - - public DateTime? Triggered { get; set; } - } - - [CollectionName("UsageTracker")] - public sealed class GrainState - { - public Dictionary Targets { get; set; } = new Dictionary(); - } - - public UsageTrackerGrain(IGrainState state, IUsageTracker usageTracker) - { - Guard.NotNull(state, nameof(state)); - Guard.NotNull(usageTracker, nameof(usageTracker)); - - this.state = state; - - this.usageTracker = usageTracker; - } - - protected override Task OnActivateAsync(string key) - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); - - return TaskHelper.Done; - } - - public Task ActivateAsync() - { - return TaskHelper.Done; - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return TaskHelper.Done; - } - - public async Task CheckUsagesAsync() - { - var today = DateTime.Today; - - foreach (var kvp in state.Value.Targets) - { - var target = kvp.Value; - - var from = GetFromDate(today, target.NumDays); - - if (!target.Triggered.HasValue || target.Triggered < from) - { - var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); - - var limit = kvp.Value.Limits; - - if (usage > limit) - { - kvp.Value.Triggered = today; - - var @event = new AppUsageExceeded - { - AppId = target.AppId, - CallsCurrent = usage, - CallsLimit = limit, - RuleId = kvp.Key - }; - - await state.WriteEventAsync(Envelope.Create(@event)); - } - } - } - - await state.WriteAsync(); - } - - private static DateTime GetFromDate(DateTime today, int? numDays) - { - if (numDays.HasValue) - { - return today.AddDays(-numDays.Value).AddDays(1); - } - else - { - return new DateTime(today.Year, today.Month, 1); - } - } - - public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays) - { - UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); - - return state.WriteAsync(); - } - - public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) - { - UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); - - return state.WriteAsync(); - } - - public Task AddTargetAsync(Guid ruleId, int limits) - { - UpdateTarget(ruleId, t => t.Limits = limits); - - return state.WriteAsync(); - } - - public Task RemoveTargetAsync(Guid ruleId) - { - state.Value.Targets.Remove(ruleId); - - return state.WriteAsync(); - } - - private void UpdateTarget(Guid ruleId, Action updater) - { - updater(state.Value.Targets.GetOrAddNew(ruleId)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs deleted file mode 100644 index 42bcb40e2..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed class UsageTriggerHandler : RuleTriggerHandler - { - private const string EventName = "Usage exceeded"; - - protected override Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedUsageExceededEvent - { - CallsCurrent = @event.Payload.CallsCurrent, - CallsLimit = @event.Payload.CallsLimit, - Name = EventName - }; - - return Task.FromResult(result); - } - - protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) - { - return @event.CallsLimit == trigger.Limit; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs deleted file mode 100644 index 445218b1d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class BackupSchemas : BackupHandler - { - private readonly Dictionary schemasByName = new Dictionary(); - private readonly ISchemasIndex indexSchemas; - - public override string Name { get; } = "Schemas"; - - public BackupSchemas(ISchemasIndex indexSchemas) - { - Guard.NotNull(indexSchemas, nameof(indexSchemas)); - - this.indexSchemas = indexSchemas; - } - - public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) - { - switch (@event.Payload) - { - case SchemaCreated schemaCreated: - schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id; - break; - case SchemaDeleted schemaDeleted: - schemasByName.Remove(schemaDeleted.SchemaId.Name); - break; - } - - return TaskHelper.True; - } - - public override Task RestoreAsync(Guid appId, BackupReader reader) - { - return indexSchemas.RebuildAsync(appId, schemasByName); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs deleted file mode 100644 index 92966a7f1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ /dev/null @@ -1,251 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -#pragma warning disable IDE0060 // Remove unused parameter - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public static class GuardSchema - { - public static void CanCreate(CreateSchema command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot create schema.", e => - { - if (!command.Name.IsSlug()) - { - e(Not.ValidSlug("Name"), nameof(command.Name)); - } - - ValidateUpsert(command, e); - }); - } - - public static void CanSynchronize(SynchronizeSchema command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot synchronize schema.", e => - { - ValidateUpsert(command, e); - }); - } - - public static void CanReorder(Schema schema, ReorderFields command) - { - Guard.NotNull(command, nameof(command)); - - IArrayField arrayField = null; - - if (command.ParentFieldId.HasValue) - { - arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); - } - - Validate.It(() => "Cannot reorder schema fields.", error => - { - if (command.FieldIds == null) - { - error("Field ids is required.", nameof(command.FieldIds)); - } - - if (arrayField == null) - { - ValidateFieldIds(error, command, schema.FieldsById); - } - else - { - ValidateFieldIds(error, command, arrayField.FieldsById); - } - }); - } - - public static void CanConfigurePreviewUrls(ConfigurePreviewUrls command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot configure preview urls.", error => - { - if (command.PreviewUrls == null) - { - error("Preview Urls is required.", nameof(command.PreviewUrls)); - } - }); - } - - public static void CanPublish(Schema schema, PublishSchema command) - { - Guard.NotNull(command, nameof(command)); - - if (schema.IsPublished) - { - throw new DomainException("Schema is already published."); - } - } - - public static void CanUnpublish(Schema schema, UnpublishSchema command) - { - Guard.NotNull(command, nameof(command)); - - if (!schema.IsPublished) - { - throw new DomainException("Schema is not published."); - } - } - - public static void CanUpdate(Schema schema, UpdateSchema command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanConfigureScripts(Schema schema, ConfigureScripts command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanChangeCategory(Schema schema, ChangeCategory command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanDelete(Schema schema, DeleteSchema command) - { - Guard.NotNull(command, nameof(command)); - } - - private static void ValidateUpsert(UpsertCommand command, AddValidation e) - { - if (command.Fields?.Count > 0) - { - var fieldIndex = 0; - var fieldPrefix = string.Empty; - - foreach (var field in command.Fields) - { - fieldIndex++; - fieldPrefix = $"Fields[{fieldIndex}]"; - - ValidateRootField(field, fieldPrefix, e); - } - - if (command.Fields.Select(x => x?.Name).Distinct().Count() != command.Fields.Count) - { - e("Fields cannot have duplicate names.", nameof(command.Fields)); - } - } - } - - private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) - { - if (field == null) - { - e(Not.Defined("Field"), prefix); - } - else - { - if (!field.Partitioning.IsValidPartitioning()) - { - e(Not.Valid("Partitioning"), $"{prefix}.{nameof(field.Partitioning)}"); - } - - ValidateField(field, prefix, e); - - if (field.Nested?.Count > 0) - { - if (field.Properties is ArrayFieldProperties) - { - var nestedIndex = 0; - var nestedPrefix = string.Empty; - - foreach (var nestedField in field.Nested) - { - nestedIndex++; - nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; - - ValidateNestedField(nestedField, nestedPrefix, e); - } - } - else if (field.Nested.Count > 0) - { - e("Only array fields can have nested fields.", $"{prefix}.{nameof(field.Partitioning)}"); - } - - if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count) - { - e("Fields cannot have duplicate names.", $"{prefix}.Nested"); - } - } - } - } - - private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) - { - if (nestedField == null) - { - e(Not.Defined("Field"), prefix); - } - else - { - if (nestedField.Properties is ArrayFieldProperties) - { - e("Nested field cannot be array fields.", $"{prefix}.{nameof(nestedField.Properties)}"); - } - - ValidateField(nestedField, prefix, e); - } - } - - private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e) - { - if (!field.Name.IsPropertyName()) - { - e("Field name must be a valid javascript property name.", $"{prefix}.{nameof(field.Name)}"); - } - - if (field.Properties == null) - { - e(Not.Defined("Field properties"), $"{prefix}.{nameof(field.Properties)}"); - } - else - { - if (!field.Properties.IsForApi()) - { - if (field.IsHidden) - { - e("UI field cannot be hidden.", $"{prefix}.{nameof(field.IsHidden)}"); - } - - if (field.IsDisabled) - { - e("UI field cannot be disabled.", $"{prefix}.{nameof(field.IsDisabled)}"); - } - } - - var errors = FieldPropertiesValidator.Validate(field.Properties); - - errors.Foreach(x => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); - } - } - - private static void ValidateFieldIds(AddValidation error, ReorderFields c, IReadOnlyDictionary fields) - { - if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x)))) - { - error("Field ids do not cover all fields.", nameof(c.FieldIds)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs deleted file mode 100644 index 46d5fbd8e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public static class GuardSchemaField - { - public static void CanAdd(Schema schema, AddField command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add a new field.", e => - { - if (!command.Name.IsPropertyName()) - { - e("Name must be a valid javascript property name.", nameof(command.Name)); - } - - if (command.Properties == null) - { - e(Not.Defined("Properties"), nameof(command.Properties)); - } - else - { - var errors = FieldPropertiesValidator.Validate(command.Properties); - - errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); - } - - if (command.ParentFieldId.HasValue) - { - var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); - - if (arrayField.FieldsByName.ContainsKey(command.Name)) - { - e("A field with the same name already exists."); - } - } - else - { - if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning()) - { - e(Not.Valid("Partitioning"), nameof(command.Partitioning)); - } - - if (schema.FieldsByName.ContainsKey(command.Name)) - { - e("A field with the same name already exists."); - } - } - }); - } - - public static void CanUpdate(Schema schema, UpdateField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - Validate.It(() => "Cannot update field.", e => - { - if (command.Properties == null) - { - e(Not.Defined("Properties"), nameof(command.Properties)); - } - else - { - var errors = FieldPropertiesValidator.Validate(command.Properties); - - errors.Foreach(x => x.WithPrefix(nameof(command.Properties)).AddTo(e)); - } - }); - } - - public static void CanHide(Schema schema, HideField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsHidden) - { - throw new DomainException("Schema field is already hidden."); - } - - if (!field.IsForApi()) - { - throw new DomainException("UI field cannot be hidden."); - } - } - - public static void CanShow(Schema schema, ShowField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (!field.IsHidden) - { - throw new DomainException("Schema field is already visible."); - } - } - - public static void CanDisable(Schema schema, DisableField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsDisabled) - { - throw new DomainException("Schema field is already disabled."); - } - - if (!field.IsForApi(true)) - { - throw new DomainException("UI field cannot be disabled."); - } - } - - public static void CanDelete(Schema schema, DeleteField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsLocked) - { - throw new DomainException("Schema field is locked."); - } - } - - public static void CanEnable(Schema schema, EnableField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (!field.IsDisabled) - { - throw new DomainException("Schema field is already enabled."); - } - } - - public static void CanLock(Schema schema, LockField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs deleted file mode 100644 index f750b9935..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public interface ISchemasIndex - { - Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); - - Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false); - - Task> GetSchemasAsync(Guid appId, bool allowDeleted = false); - - Task RebuildAsync(Guid appId, Dictionary schemas); - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs deleted file mode 100644 index 6a09a538f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ /dev/null @@ -1,181 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex - { - private readonly IGrainFactory grainFactory; - - public SchemasIndex(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public Task RebuildAsync(Guid appId, Dictionary schemas) - { - return Index(appId).RebuildAsync(schemas); - } - - public async Task> GetSchemasAsync(Guid appId, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var ids = await GetSchemaIdsAsync(appId); - - var schemas = - await Task.WhenAll( - ids.Select(id => GetSchemaAsync(appId, id, allowDeleted))); - - return schemas.Where(x => x != null).ToList(); - } - } - - public async Task GetSchemaByNameAsync(Guid appId, string name, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var id = await GetSchemaIdAsync(appId, name); - - if (id == default) - { - return null; - } - - return await GetSchemaAsync(appId, id, allowDeleted); - } - } - - public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) - { - using (Profiler.TraceMethod()) - { - var schema = await grainFactory.GetGrain(id).GetStateAsync(); - - if (IsFound(schema.Value, allowDeleted)) - { - return schema.Value; - } - - return null; - } - } - - private async Task GetSchemaIdAsync(Guid appId, string name) - { - using (Profiler.TraceMethod()) - { - return await Index(appId).GetIdAsync(name); - } - } - - private async Task> GetSchemaIdsAsync(Guid appId) - { - using (Profiler.TraceMethod()) - { - return await Index(appId).GetIdsAsync(); - } - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is CreateSchema createSchema) - { - var index = Index(createSchema.AppId.Id); - - string token = await CheckSchemaAsync(index, createSchema); - - try - { - await next(); - } - finally - { - if (token != null) - { - if (context.IsCompleted) - { - await index.AddAsync(token); - } - else - { - await index.RemoveReservationAsync(token); - } - } - } - } - else - { - await next(); - - if (context.IsCompleted) - { - if (context.Command is DeleteSchema deleteSchema) - { - await DeleteSchemaAsync(deleteSchema); - } - } - } - } - - private async Task CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command) - { - var name = command.Name; - - if (name.IsSlug()) - { - var token = await index.ReserveAsync(command.SchemaId, name); - - if (token == null) - { - var error = new ValidationError("A schema with this name already exists."); - - throw new ValidationException("Cannot create schema.", error); - } - - return token; - } - - return null; - } - - private async Task DeleteSchemaAsync(DeleteSchema commmand) - { - var schemaId = commmand.SchemaId; - - var schema = await grainFactory.GetGrain(schemaId).GetStateAsync(); - - if (IsFound(schema.Value, true)) - { - await Index(schema.Value.AppId.Id).RemoveAsync(schemaId); - } - } - - private ISchemasByAppIndexGrain Index(Guid appId) - { - return grainFactory.GetGrain(appId); - } - - private static bool IsFound(ISchemaEntity entity, bool allowDeleted) - { - return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs deleted file mode 100644 index 46dd6c0e7..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - - public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - - this.scriptEngine = scriptEngine; - } - - protected override Task CreateEnrichedEventAsync(Envelope @event) - { - var result = new EnrichedSchemaEvent(); - - SimpleMapper.Map(@event.Payload, result); - - switch (@event.Payload) - { - case FieldEvent _: - case SchemaPreviewUrlsConfigured _: - case SchemaScriptsConfigured _: - case SchemaUpdated _: - case ParentFieldEvent _: - result.Type = EnrichedSchemaEventType.Updated; - break; - case SchemaCreated _: - result.Type = EnrichedSchemaEventType.Created; - break; - case SchemaPublished _: - result.Type = EnrichedSchemaEventType.Published; - break; - case SchemaUnpublished _: - result.Type = EnrichedSchemaEventType.Unpublished; - break; - case SchemaDeleted _: - result.Type = EnrichedSchemaEventType.Deleted; - break; - default: - result = null; - break; - } - - result.Name = $"Schema{result.Type}"; - - return Task.FromResult(result); - } - - protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) - { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs deleted file mode 100644 index 1689b4cd1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ /dev/null @@ -1,417 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.EventSynchronization; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.Guards; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain - { - private readonly IJsonSerializer serializer; - - public SchemaGrain(IStore store, ISemanticLog log, IJsonSerializer serializer) - : base(store, log) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - VerifyNotDeleted(); - - switch (command) - { - case AddField addField: - return UpdateReturn(addField, c => - { - GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); - - Add(c); - - long id; - - if (c.ParentFieldId == null) - { - id = Snapshot.SchemaDef.FieldsByName[c.Name].Id; - } - else - { - id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id; - } - - return Snapshot; - }); - - case CreateSchema createSchema: - return CreateReturn(createSchema, c => - { - GuardSchema.CanCreate(c); - - Create(c); - - return Snapshot; - }); - - case SynchronizeSchema synchronizeSchema: - return UpdateReturn(synchronizeSchema, c => - { - GuardSchema.CanSynchronize(c); - - Synchronize(c); - - return Snapshot; - }); - - case DeleteField deleteField: - return UpdateReturn(deleteField, c => - { - GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); - - DeleteField(c); - - return Snapshot; - }); - - case LockField lockField: - return UpdateReturn(lockField, c => - { - GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); - - LockField(c); - - return Snapshot; - }); - - case HideField hideField: - return UpdateReturn(hideField, c => - { - GuardSchemaField.CanHide(Snapshot.SchemaDef, c); - - HideField(c); - - return Snapshot; - }); - - case ShowField showField: - return UpdateReturn(showField, c => - { - GuardSchemaField.CanShow(Snapshot.SchemaDef, c); - - ShowField(c); - - return Snapshot; - }); - - case DisableField disableField: - return UpdateReturn(disableField, c => - { - GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); - - DisableField(c); - - return Snapshot; - }); - - case EnableField enableField: - return UpdateReturn(enableField, c => - { - GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); - - EnableField(c); - - return Snapshot; - }); - - case UpdateField updateField: - return UpdateReturn(updateField, c => - { - GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); - - UpdateField(c); - - return Snapshot; - }); - - case ReorderFields reorderFields: - return UpdateReturn(reorderFields, c => - { - GuardSchema.CanReorder(Snapshot.SchemaDef, c); - - Reorder(c); - - return Snapshot; - }); - - case UpdateSchema updateSchema: - return UpdateReturn(updateSchema, c => - { - GuardSchema.CanUpdate(Snapshot.SchemaDef, c); - - Update(c); - - return Snapshot; - }); - - case PublishSchema publishSchema: - return UpdateReturn(publishSchema, c => - { - GuardSchema.CanPublish(Snapshot.SchemaDef, c); - - Publish(c); - - return Snapshot; - }); - - case UnpublishSchema unpublishSchema: - return UpdateReturn(unpublishSchema, c => - { - GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); - - Unpublish(c); - - return Snapshot; - }); - - case ConfigureScripts configureScripts: - return UpdateReturn(configureScripts, c => - { - GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); - - ConfigureScripts(c); - - return Snapshot; - }); - - case ChangeCategory changeCategory: - return UpdateReturn(changeCategory, c => - { - GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c); - - ChangeCategory(c); - - return Snapshot; - }); - - case ConfigurePreviewUrls configurePreviewUrls: - return UpdateReturn(configurePreviewUrls, c => - { - GuardSchema.CanConfigurePreviewUrls(c); - - ConfigurePreviewUrls(c); - - return Snapshot; - }); - - case DeleteSchema deleteSchema: - return Update(deleteSchema, c => - { - GuardSchema.CanDelete(Snapshot.SchemaDef, c); - - Delete(c); - }); - - default: - throw new NotSupportedException(); - } - } - - public void Synchronize(SynchronizeSchema command) - { - var options = new SchemaSynchronizationOptions - { - NoFieldDeletion = command.NoFieldDeletion, - NoFieldRecreation = command.NoFieldRecreation - }; - - var schemaSource = Snapshot.SchemaDef; - var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); - - var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); - - foreach (var @event in events) - { - RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); - } - } - - public void Create(CreateSchema command) - { - RaiseEvent(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.ToSchema() }); - } - - public void Add(AddField command) - { - RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) }); - } - - public void UpdateField(UpdateField command) - { - RaiseEvent(command, new FieldUpdated()); - } - - public void LockField(LockField command) - { - RaiseEvent(command, new FieldLocked()); - } - - public void HideField(HideField command) - { - RaiseEvent(command, new FieldHidden()); - } - - public void ShowField(ShowField command) - { - RaiseEvent(command, new FieldShown()); - } - - public void DisableField(DisableField command) - { - RaiseEvent(command, new FieldDisabled()); - } - - public void EnableField(EnableField command) - { - RaiseEvent(command, new FieldEnabled()); - } - - public void DeleteField(DeleteField command) - { - RaiseEvent(command, new FieldDeleted()); - } - - public void Reorder(ReorderFields command) - { - RaiseEvent(command, new SchemaFieldsReordered()); - } - - public void Publish(PublishSchema command) - { - RaiseEvent(command, new SchemaPublished()); - } - - public void Unpublish(UnpublishSchema command) - { - RaiseEvent(command, new SchemaUnpublished()); - } - - public void ConfigureScripts(ConfigureScripts command) - { - RaiseEvent(command, new SchemaScriptsConfigured()); - } - - public void ChangeCategory(ChangeCategory command) - { - RaiseEvent(command, new SchemaCategoryChanged()); - } - - public void ConfigurePreviewUrls(ConfigurePreviewUrls command) - { - RaiseEvent(command, new SchemaPreviewUrlsConfigured()); - } - - public void Update(UpdateSchema command) - { - RaiseEvent(command, new SchemaUpdated()); - } - - public void Delete(DeleteSchema command) - { - RaiseEvent(command, new SchemaDeleted()); - } - - private void RaiseEvent(TCommand command, TEvent @event) where TCommand : SchemaCommand where TEvent : SchemaEvent - { - SimpleMapper.Map(command, @event); - - NamedId GetFieldId(long? id) - { - if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) - { - return field.NamedId(); - } - - return null; - } - - if (command is ParentFieldCommand pc && @event is ParentFieldEvent pe) - { - if (pc.ParentFieldId.HasValue) - { - if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field)) - { - pe.ParentFieldId = field.NamedId(); - - if (command is FieldCommand fc && @event is FieldEvent fe) - { - if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) - { - fe.FieldId = nestedField.NamedId(); - } - } - } - } - else if (command is FieldCommand fc && @event is FieldEvent fe) - { - fe.FieldId = GetFieldId(fc.FieldId); - } - } - - RaiseEvent(@event); - } - - private void RaiseEvent(SchemaEvent @event) - { - if (@event.SchemaId == null) - { - @event.SchemaId = Snapshot.NamedId(); - } - - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private NamedId CreateFieldId(AddField command) - { - return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name); - } - - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException("Schema has already been deleted."); - } - } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs deleted file mode 100644 index d452f452c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase - { - public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "reordered fields of schema {[Name]}."); - - AddEventMessage( - "created schema {[Name]}."); - - AddEventMessage( - "updated schema {[Name]}."); - - AddEventMessage( - "deleted schema {[Name]}."); - - AddEventMessage( - "published schema {[Name]}."); - - AddEventMessage( - "unpublished schema {[Name]}."); - - AddEventMessage( - "reordered fields of schema {[Name]}."); - - AddEventMessage( - "configured script of schema {[Name]}."); - - AddEventMessage( - "added field {[Field]} to schema {[Name]}."); - - AddEventMessage( - "deleted field {[Field]} from schema {[Name]}."); - - AddEventMessage( - "has locked field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "has hidden field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "has shown field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "disabled field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "disabled field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "has updated field {[Field]} of schema {[Name]}."); - - AddEventMessage( - "deleted field {[Field]} of schema {[Name]}."); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - if (@event.Payload is SchemaEvent schemaEvent) - { - var channel = $"schemas.{schemaEvent.SchemaId.Name}"; - - var result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); - - if (schemaEvent is FieldEvent fieldEvent) - { - result.Param("Field", fieldEvent.FieldId.Name); - } - - return Task.FromResult(result); - } - - return Task.FromResult(null); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj deleted file mode 100644 index e92f011f5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs deleted file mode 100644 index 08f1ff835..000000000 --- a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public sealed class GrainTagService : ITagService - { - private readonly IGrainFactory grainFactory; - - public string Name - { - get { return "Tags"; } - } - - public GrainTagService(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public Task> NormalizeTagsAsync(Guid appId, string group, HashSet names, HashSet ids) - { - return GetGrain(appId, group).NormalizeTagsAsync(names, ids); - } - - public Task> GetTagIdsAsync(Guid appId, string group, HashSet names) - { - return GetGrain(appId, group).GetTagIdsAsync(names); - } - - public Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids) - { - return GetGrain(appId, group).DenormalizeTagsAsync(ids); - } - - public Task GetTagsAsync(Guid appId, string group) - { - return GetGrain(appId, group).GetTagsAsync(); - } - - public Task GetExportableTagsAsync(Guid appId, string group) - { - return GetGrain(appId, group).GetExportableTagsAsync(); - } - - public Task RebuildTagsAsync(Guid appId, string group, TagsExport tags) - { - return GetGrain(appId, group).RebuildAsync(tags); - } - - public Task ClearAsync(Guid appId, string group) - { - return GetGrain(appId, group).ClearAsync(); - } - - private ITagGrain GetGrain(Guid appId, string group) - { - Guard.NotNullOrEmpty(group, nameof(group)); - - return grainFactory.GetGrain($"{appId}_{group}"); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs deleted file mode 100644 index be9a5bdfb..000000000 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.Tags; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public interface ITagGrain : IGrainWithStringKey - { - Task> NormalizeTagsAsync(HashSet names, HashSet ids); - - Task> GetTagIdsAsync(HashSet names); - - Task> DenormalizeTagsAsync(HashSet ids); - - Task GetTagsAsync(); - - Task GetExportableTagsAsync(); - - Task ClearAsync(); - - Task RebuildAsync(TagsExport tags); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs deleted file mode 100644 index 571316e20..000000000 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ /dev/null @@ -1,152 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public sealed class TagGrain : GrainOfString, ITagGrain - { - private readonly IGrainState state; - - [CollectionName("Index_Tags")] - public sealed class GrainState - { - public TagsExport Tags { get; set; } = new TagsExport(); - } - - public TagGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task RebuildAsync(TagsExport tags) - { - state.Value.Tags = tags; - - return state.WriteAsync(); - } - - public async Task> NormalizeTagsAsync(HashSet names, HashSet ids) - { - var result = new Dictionary(); - - if (names != null) - { - foreach (var tag in names) - { - if (!string.IsNullOrWhiteSpace(tag)) - { - var tagName = tag.ToLowerInvariant(); - var tagId = string.Empty; - - var found = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); - - if (found.Value != null) - { - tagId = found.Key; - - if (ids == null || !ids.Contains(tagId)) - { - found.Value.Count++; - } - } - else - { - tagId = Guid.NewGuid().ToString(); - - state.Value.Tags.Add(tagId, new Tag { Name = tagName }); - } - - result.Add(tagName, tagId); - } - } - } - - if (ids != null) - { - foreach (var id in ids) - { - if (!result.ContainsValue(id)) - { - if (state.Value.Tags.TryGetValue(id, out var tagInfo)) - { - tagInfo.Count--; - - if (tagInfo.Count <= 0) - { - state.Value.Tags.Remove(id); - } - } - } - } - } - - await state.WriteAsync(); - - return result; - } - - public Task> GetTagIdsAsync(HashSet names) - { - var result = new Dictionary(); - - foreach (var name in names) - { - var id = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; - - if (!string.IsNullOrWhiteSpace(id)) - { - result.Add(name, id); - } - } - - return Task.FromResult(result); - } - - public Task> DenormalizeTagsAsync(HashSet ids) - { - var result = new Dictionary(); - - foreach (var id in ids) - { - if (state.Value.Tags.TryGetValue(id, out var tagInfo)) - { - result[id] = tagInfo.Name; - } - } - - return Task.FromResult(result); - } - - public Task GetTagsAsync() - { - var tags = state.Value.Tags.Values.ToDictionary(x => x.Name, x => x.Count); - - return Task.FromResult(new TagsSet(tags, state.Version)); - } - - public Task GetExportableTagsAsync() - { - return Task.FromResult(state.Value.Tags); - } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs deleted file mode 100644 index 4075b82f6..000000000 --- a/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Apps -{ - [EventType(nameof(AppPatternAdded))] - public sealed class AppPatternAdded : AppEvent - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs deleted file mode 100644 index 81a9c6c39..000000000 --- a/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Apps -{ - [EventType(nameof(AppPatternUpdated))] - public sealed class AppPatternUpdated : AppEvent - { - public Guid PatternId { get; set; } - - public string Name { get; set; } - - public string Pattern { get; set; } - - public string Message { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs deleted file mode 100644 index d2cec90a8..000000000 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Assets -{ - [EventType(nameof(AssetAnnotated))] - public sealed class AssetAnnotated : AssetEvent - { - public string FileName { get; set; } - - public string Slug { get; set; } - - public HashSet Tags { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs deleted file mode 100644 index 5200031cc..000000000 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Assets -{ - [EventType(nameof(AssetCreated))] - public sealed class AssetCreated : AssetEvent - { - public string FileName { get; set; } - - public string FileHash { get; set; } - - public string MimeType { get; set; } - - public string Slug { get; set; } - - public long FileVersion { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } - - public HashSet Tags { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs b/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs deleted file mode 100644 index d73358c30..000000000 --- a/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Events.Schemas -{ - [EventType(nameof(FieldAdded))] - public sealed class FieldAdded : FieldEvent - { - public string Name { get; set; } - - public string Partitioning { get; set; } - - public FieldProperties Properties { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs b/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs deleted file mode 100644 index 0406c3c40..000000000 --- a/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Events.Schemas -{ - public abstract class ParentFieldEvent : SchemaEvent - { - public NamedId ParentFieldId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj deleted file mode 100644 index b0cf01c94..000000000 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/src/Squidex.Domain.Users.MongoDb/MongoUser.cs deleted file mode 100644 index f3a7a1876..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUser : IdentityUser - { - public List Claims { get; set; } = new List(); - - public List Tokens { get; set; } = new List(); - - public List Logins { get; set; } = new List(); - - public HashSet Roles { get; set; } = new HashSet(); - - internal void AddLogin(UserLoginInfo login) - { - Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName)); - } - - internal void AddRole(string role) - { - Roles.Add(role); - } - - internal void RemoveRole(string role) - { - Roles.Remove(role); - } - - internal void RemoveLogin(string loginProvider, string providerKey) - { - Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - } - - internal void AddClaim(Claim claim) - { - Claims.Add(claim); - } - - internal void AddClaims(IEnumerable claims) - { - claims.Foreach(AddClaim); - } - - internal void RemoveClaim(Claim claim) - { - Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); - } - - internal void RemoveClaims(IEnumerable claims) - { - claims.Foreach(RemoveClaim); - } - - internal string GetToken(string loginProvider, string name) - { - return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value; - } - - internal void AddToken(string loginProvider, string name, string value) - { - Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value }); - } - - internal void RemoveToken(string loginProvider, string name) - { - Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); - } - - internal void ReplaceClaim(Claim existingClaim, Claim newClaim) - { - RemoveClaim(existingClaim); - - AddClaim(newClaim); - } - - internal void SetToken(string loginProider, string name, string value) - { - RemoveToken(loginProider, name); - - AddToken(loginProider, name, value); - } - } - - public sealed class UserTokenInfo : IdentityUserToken - { - } -} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs deleted file mode 100644 index 30f29c2c0..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ /dev/null @@ -1,526 +0,0 @@ -// ========================================================================== -// 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 System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUserStore : - MongoRepositoryBase, - IUserAuthenticationTokenStore, - IUserAuthenticatorKeyStore, - IUserClaimStore, - IUserEmailStore, - IUserFactory, - IUserLockoutStore, - IUserLoginStore, - IUserPasswordStore, - IUserPhoneNumberStore, - IUserRoleStore, - IUserSecurityStampStore, - IUserTwoFactorStore, - IUserTwoFactorRecoveryCodeStore, - IQueryableUserStore - { - private const string InternalLoginProvider = "[AspNetUserStore]"; - private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; - private const string RecoveryCodeTokenName = "RecoveryCodes"; - - static MongoUserStore() - { - BsonClassMap.RegisterClassMap(cm => - { - cm.MapConstructor(typeof(Claim).GetConstructors() - .First(x => - { - var parameters = x.GetParameters(); - - return parameters.Length == 2 && - parameters[0].Name == "type" && - parameters[0].ParameterType == typeof(string) && - parameters[1].Name == "value" && - parameters[1].ParameterType == typeof(string); - })) - .SetArguments(new[] - { - nameof(Claim.Type), - nameof(Claim.Value) - }); - - cm.MapMember(x => x.Type); - cm.MapMember(x => x.Value); - }); - - BsonClassMap.RegisterClassMap(cm => - { - cm.MapConstructor(typeof(UserLoginInfo).GetConstructors().First()) - .SetArguments(new[] - { - nameof(UserLoginInfo.LoginProvider), - nameof(UserLoginInfo.ProviderKey), - nameof(UserLoginInfo.ProviderDisplayName) - }); - - cm.AutoMap(); - }); - - BsonClassMap.RegisterClassMap>(cm => - { - cm.AutoMap(); - - cm.UnmapMember(x => x.UserId); - }); - - BsonClassMap.RegisterClassMap>(cm => - { - cm.AutoMap(); - - cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId)); - cm.MapMember(x => x.AccessFailedCount).SetIgnoreIfDefault(true); - cm.MapMember(x => x.EmailConfirmed).SetIgnoreIfDefault(true); - cm.MapMember(x => x.LockoutEnd).SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); - cm.MapMember(x => x.LockoutEnabled).SetIgnoreIfDefault(true); - cm.MapMember(x => x.PasswordHash).SetIgnoreIfNull(true); - cm.MapMember(x => x.PhoneNumber).SetIgnoreIfNull(true); - cm.MapMember(x => x.PhoneNumberConfirmed).SetIgnoreIfDefault(true); - cm.MapMember(x => x.SecurityStamp).SetIgnoreIfNull(true); - cm.MapMember(x => x.TwoFactorEnabled).SetIgnoreIfDefault(true); - }); - } - - public MongoUserStore(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Identity_Users"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending("Logins.LoginProvider") - .Ascending("Logins.ProviderKey")), - new CreateIndexModel( - Index - .Ascending(x => x.NormalizedUserName), - new CreateIndexOptions - { - Unique = true - }), - new CreateIndexModel( - Index - .Ascending(x => x.NormalizedEmail), - new CreateIndexOptions - { - Unique = true - }) - }, ct); - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; - } - - public void Dispose() - { - } - - public IQueryable Users - { - get { return Collection.AsQueryable(); } - } - - public bool IsId(string id) - { - return ObjectId.TryParse(id, out _); - } - - public IdentityUser Create(string email) - { - return new MongoUser { Email = email, UserName = email }; - } - - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) - { - if (!IsId(userId)) - { - return null; - } - - return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); - } - - public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) - { - return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); - } - - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) - { - return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); - } - - public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); - } - - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) - { - return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); - } - - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) - { - return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); - } - - public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) - { - user.Id = ObjectId.GenerateNewId().ToString(); - - await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); - - return IdentityResult.Success; - } - - public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) - { - await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); - - return IdentityResult.Success; - } - - public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) - { - await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); - - return IdentityResult.Success; - } - - public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).Id); - } - - public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).UserName); - } - - public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).NormalizedUserName); - } - - public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).PasswordHash); - } - - public Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult>(((MongoUser)user).Roles.ToList()); - } - - public Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); - } - - public Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList()); - } - - public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).SecurityStamp); - } - - public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).Email); - } - - public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).EmailConfirmed); - } - - public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).NormalizedEmail); - } - - public Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult>(((MongoUser)user).Claims); - } - - public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).PhoneNumber); - } - - public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); - } - - public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).TwoFactorEnabled); - } - - public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).LockoutEnd); - } - - public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).AccessFailedCount); - } - - public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).LockoutEnabled); - } - - public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)); - } - - public Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)); - } - - public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); - } - - public Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) - { - return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0); - } - - public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken) - { - ((MongoUser)user).UserName = userName; - - return TaskHelper.Done; - } - - public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken) - { - ((MongoUser)user).NormalizedUserName = normalizedName; - - return TaskHelper.Done; - } - - public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken) - { - ((MongoUser)user).PasswordHash = passwordHash; - - return TaskHelper.Done; - } - - public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - ((MongoUser)user).AddRole(roleName); - - return TaskHelper.Done; - } - - public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveRole(roleName); - - return TaskHelper.Done; - } - - public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) - { - ((MongoUser)user).AddLogin(login); - - return TaskHelper.Done; - } - - public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveLogin(loginProvider, providerKey); - - return TaskHelper.Done; - } - - public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) - { - ((MongoUser)user).SecurityStamp = stamp; - - return TaskHelper.Done; - } - - public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken) - { - ((MongoUser)user).Email = email; - - return TaskHelper.Done; - } - - public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - ((MongoUser)user).EmailConfirmed = confirmed; - - return TaskHelper.Done; - } - - public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - ((MongoUser)user).NormalizedEmail = normalizedEmail; - - return TaskHelper.Done; - } - - public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) - { - ((MongoUser)user).AddClaims(claims); - - return TaskHelper.Done; - } - - public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) - { - ((MongoUser)user).ReplaceClaim(claim, newClaim); - - return TaskHelper.Done; - } - - public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveClaims(claims); - - return TaskHelper.Done; - } - - public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken) - { - ((MongoUser)user).PhoneNumber = phoneNumber; - - return TaskHelper.Done; - } - - public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) - { - ((MongoUser)user).PhoneNumberConfirmed = confirmed; - - return TaskHelper.Done; - } - - public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) - { - ((MongoUser)user).TwoFactorEnabled = enabled; - - return TaskHelper.Done; - } - - public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) - { - ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; - - return TaskHelper.Done; - } - - public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - ((MongoUser)user).AccessFailedCount++; - - return Task.FromResult(((MongoUser)user).AccessFailedCount); - } - - public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) - { - ((MongoUser)user).AccessFailedCount = 0; - - return TaskHelper.Done; - } - - public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) - { - ((MongoUser)user).LockoutEnabled = enabled; - - return TaskHelper.Done; - } - - public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) - { - ((MongoUser)user).SetToken(loginProvider, name, value); - - return TaskHelper.Done; - } - - public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveToken(loginProvider, name); - - return TaskHelper.Done; - } - - public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) - { - ((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); - - return TaskHelper.Done; - } - - public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) - { - ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); - - return TaskHelper.Done; - } - - public Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) - { - var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; - - var splitCodes = mergedCodes.Split(';'); - if (splitCodes.Contains(code)) - { - var updatedCodes = new List(splitCodes.Where(s => s != code)); - - ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); - - return TaskHelper.True; - } - - return TaskHelper.False; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj deleted file mode 100644 index 88d90e5f7..000000000 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Domain.Users/AssetUserPictureStore.cs b/src/Squidex.Domain.Users/AssetUserPictureStore.cs deleted file mode 100644 index 16219022b..000000000 --- a/src/Squidex.Domain.Users/AssetUserPictureStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; - -namespace Squidex.Domain.Users -{ - public sealed class AssetUserPictureStore : IUserPictureStore - { - private readonly IAssetStore assetStore; - - public AssetUserPictureStore(IAssetStore assetStore) - { - Guard.NotNull(assetStore, nameof(assetStore)); - - this.assetStore = assetStore; - } - - public Task UploadAsync(string userId, Stream stream) - { - return assetStore.UploadAsync(userId, 0, "picture", stream, true); - } - - public async Task DownloadAsync(string userId) - { - var memoryStream = new MemoryStream(); - - await assetStore.DownloadAsync(userId, 0, "picture", memoryStream); - - memoryStream.Position = 0; - - return memoryStream; - } - } -} diff --git a/src/Squidex.Domain.Users/DefaultUserResolver.cs b/src/Squidex.Domain.Users/DefaultUserResolver.cs deleted file mode 100644 index 1112ee86b..000000000 --- a/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultUserResolver : IUserResolver - { - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - - public DefaultUserResolver(UserManager userManager, IUserFactory userFactory) - { - Guard.NotNull(userManager, nameof(userManager)); - Guard.NotNull(userFactory, nameof(userFactory)); - - this.userManager = userManager; - this.userFactory = userFactory; - } - - public async Task CreateUserIfNotExists(string email, bool invited) - { - var user = userFactory.Create(email); - - try - { - var result = await userManager.CreateAsync(user); - - if (result.Succeeded) - { - var values = new UserValues { DisplayName = email, Invited = invited }; - - await userManager.UpdateAsync(user, values); - } - - return result.Succeeded; - } - catch - { - return false; - } - } - - public async Task FindByIdOrEmailAsync(string idOrEmail) - { - if (userFactory.IsId(idOrEmail)) - { - return await userManager.FindByIdWithClaimsAsync(idOrEmail); - } - else - { - return await userManager.FindByEmailWithClaimsAsyncAsync(idOrEmail); - } - } - - public async Task> QueryByEmailAsync(string email) - { - var result = await userManager.QueryByEmailAsync(email); - - return result.OfType().ToList(); - } - - public async Task> QueryManyAsync(string[] ids) - { - var result = await userManager.QueryByIdsAync(ids); - - return result.OfType().ToDictionary(x => x.Id); - } - } -} diff --git a/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/src/Squidex.Domain.Users/DefaultXmlRepository.cs deleted file mode 100644 index 95ee76023..000000000 --- a/src/Squidex.Domain.Users/DefaultXmlRepository.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Xml.Linq; -using Microsoft.AspNetCore.DataProtection.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultXmlRepository : IXmlRepository - { - private readonly ISnapshotStore store; - - [CollectionName("XmlRepository")] - public sealed class State - { - public string Xml { get; set; } - } - - public DefaultXmlRepository(ISnapshotStore store) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - public IReadOnlyCollection GetAllElements() - { - var result = new List(); - - store.ReadAllAsync((state, version) => - { - result.Add(XElement.Parse(state.Xml)); - - return TaskHelper.Done; - }).Wait(); - - return result; - } - - public void StoreElement(XElement element, string friendlyName) - { - store.WriteAsync(friendlyName, new State { Xml = element.ToString() }, EtagVersion.Any, EtagVersion.Any).Wait(); - } - } -} diff --git a/src/Squidex.Domain.Users/PwnedPasswordValidator.cs b/src/Squidex.Domain.Users/PwnedPasswordValidator.cs deleted file mode 100644 index 08a2f8320..000000000 --- a/src/Squidex.Domain.Users/PwnedPasswordValidator.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using SharpPwned.NET; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Domain.Users -{ - public sealed class PwnedPasswordValidator : IPasswordValidator - { - private const string ErrorCode = "PwnedError"; - private const string ErrorText = "This password has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it!"; - private static readonly IdentityResult Error = IdentityResult.Failed(new IdentityError { Code = ErrorCode, Description = ErrorText }); - - private readonly HaveIBeenPwnedRestClient client = new HaveIBeenPwnedRestClient(); - private readonly ISemanticLog log; - - public PwnedPasswordValidator(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public async Task ValidateAsync(UserManager manager, IdentityUser user, string password) - { - try - { - var isBreached = await client.IsPasswordPwned(password); - - if (isBreached) - { - return Error; - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("operation", "CheckPasswordPwned") - .WriteProperty("status", "Failed")); - } - - return IdentityResult.Success; - } - } -} diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj deleted file mode 100644 index 135fee181..000000000 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs deleted file mode 100644 index 880663a92..000000000 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ========================================================================== -// 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 System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Identity; - -namespace Squidex.Domain.Users -{ - public static class UserManagerExtensions - { - public static async Task GetUserWithClaimsAsync(this UserManager userManager, ClaimsPrincipal principal) - { - if (principal == null) - { - return null; - } - - var user = await userManager.FindByIdWithClaimsAsync(userManager.GetUserId(principal)); - - return user; - } - - public static async Task ResolveUserAsync(this UserManager userManager, IdentityUser user) - { - if (user == null) - { - return null; - } - - var claims = await userManager.GetClaimsAsync(user); - - return new UserWithClaims(user, claims); - } - - public static async Task FindByIdWithClaimsAsync(this UserManager userManager, string id) - { - if (id == null) - { - return null; - } - - var user = await userManager.FindByIdAsync(id); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task FindByEmailWithClaimsAsyncAsync(this UserManager userManager, string email) - { - if (email == null) - { - return null; - } - - var user = await userManager.FindByEmailAsync(email); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task FindByLoginWithClaimsAsync(this UserManager userManager, string loginProvider, string providerKey) - { - if (loginProvider == null || providerKey == null) - { - return null; - } - - var user = await userManager.FindByLoginAsync(loginProvider, providerKey); - - return await userManager.ResolveUserAsync(user); - } - - public static Task CountByEmailAsync(this UserManager userManager, string email = null) - { - var count = QueryUsers(userManager, email).LongCount(); - - return Task.FromResult(count); - } - - public static async Task> QueryByIdsAync(this UserManager userManager, string[] ids) - { - var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); - - var result = await userManager.ResolveUsersAsync(users); - - return result.ToList(); - } - - public static async Task> QueryByEmailAsync(this UserManager userManager, string email = null, int take = 10, int skip = 0) - { - var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); - - var result = await userManager.ResolveUsersAsync(users); - - return result.ToList(); - } - - public static Task ResolveUsersAsync(this UserManager userManager, IEnumerable users) - { - return Task.WhenAll(users.Select(async user => - { - return await userManager.ResolveUserAsync(user); - })); - } - - public static IQueryable QueryUsers(UserManager userManager, string email = null) - { - var result = userManager.Users; - - if (!string.IsNullOrWhiteSpace(email)) - { - var normalizedEmail = userManager.NormalizeKey(email); - - result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); - } - - return result; - } - - public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, UserValues values) - { - var user = factory.Create(values.Email); - - try - { - await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); - - var claims = values.ToClaims(true); - - if (claims.Count > 0) - { - await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user."); - } - - if (!string.IsNullOrWhiteSpace(values.Password)) - { - await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user."); - } - } - catch - { - await userManager.DeleteAsync(user); - - throw; - } - - return await userManager.ResolveUserAsync(user); - } - - public static async Task UpdateAsync(this UserManager userManager, string id, UserValues values) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); - } - - await UpdateAsync(userManager, user, values); - - return await userManager.ResolveUserAsync(user); - } - - public static Task GenerateClientSecretAsync(this UserManager userManager, IdentityUser user) - { - var claims = new List { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) }; - - return userManager.SyncClaimsAsync(user, claims); - } - - public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) - { - try - { - await userManager.UpdateAsync(user, values); - - return IdentityResult.Success; - } - catch (ValidationException ex) - { - return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray()); - } - } - - public static async Task UpdateAsync(this UserManager userManager, IdentityUser user, UserValues values) - { - if (user == null) - { - throw new DomainObjectNotFoundException("Id", typeof(IdentityUser)); - } - - if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) - { - await DoChecked(() => userManager.SetEmailAsync(user, values.Email), "Cannot update email."); - await DoChecked(() => userManager.SetUserNameAsync(user, values.Email), "Cannot update email."); - } - - await DoChecked(() => userManager.SyncClaimsAsync(user, values.ToClaims(false)), "Cannot update user."); - - if (!string.IsNullOrWhiteSpace(values.Password)) - { - await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot replace password."); - await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password."); - } - } - - public static async Task LockAsync(this UserManager userManager, string id) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); - } - - await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user."); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task UnlockAsync(this UserManager userManager, string id) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); - } - - await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); - - return await userManager.ResolveUserAsync(user); - } - - private static async Task DoChecked(Func> action, string message) - { - var result = await action(); - - if (!result.Succeeded) - { - throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray()); - } - } - - public static async Task SyncClaimsAsync(this UserManager userManager, IdentityUser user, List claims) - { - if (claims.Any()) - { - var oldClaims = await userManager.GetClaimsAsync(user); - - var oldClaimsToRemove = new List(); - - foreach (var oldClaim in oldClaims) - { - if (claims.Any(x => x.Type == oldClaim.Type)) - { - oldClaimsToRemove.Add(oldClaim); - } - } - - if (oldClaimsToRemove.Count > 0) - { - var result = await userManager.RemoveClaimsAsync(user, oldClaimsToRemove); - - if (!result.Succeeded) - { - return result; - } - } - - return await userManager.AddClaimsAsync(user, claims.Where(x => !string.IsNullOrWhiteSpace(x.Value))); - } - - return IdentityResult.Success; - } - } -} diff --git a/src/Squidex.Domain.Users/UserWithClaims.cs b/src/Squidex.Domain.Users/UserWithClaims.cs deleted file mode 100644 index cc42fedce..000000000 --- a/src/Squidex.Domain.Users/UserWithClaims.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users -{ - public sealed class UserWithClaims : IUser - { - public IdentityUser Identity { get; } - - public List Claims { get; } - - public string Id - { - get { return Identity.Id; } - } - - public string Email - { - get { return Identity.Email; } - } - - public bool IsLocked - { - get { return Identity.LockoutEnd > DateTime.Now.ToUniversalTime(); } - } - - IReadOnlyList IUser.Claims - { - get { return Claims; } - } - - public UserWithClaims(IdentityUser user, IEnumerable claims) - { - Guard.NotNull(user, nameof(user)); - Guard.NotNull(claims, nameof(claims)); - - Identity = user; - - Claims = claims.ToList(); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs deleted file mode 100644 index c50b554a1..000000000 --- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace Squidex.Infrastructure.Assets -{ - public class AzureBlobAssetStore : IAssetStore, IInitializable - { - private readonly string containerName; - private readonly string connectionString; - private CloudBlobContainer blobContainer; - - public AzureBlobAssetStore(string connectionString, string containerName) - { - Guard.NotNullOrEmpty(containerName, nameof(containerName)); - Guard.NotNullOrEmpty(connectionString, nameof(connectionString)); - - this.connectionString = connectionString; - this.containerName = containerName; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - var storageAccount = CloudStorageAccount.Parse(connectionString); - - var blobClient = storageAccount.CreateCloudBlobClient(); - var blobReference = blobClient.GetContainerReference(containerName); - - await blobReference.CreateIfNotExistsAsync(); - - blobContainer = blobReference; - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - if (blobContainer.Properties.PublicAccess != BlobContainerPublicAccessType.Blob) - { - var blob = blobContainer.GetBlockBlobReference(fileName); - - return blob.Uri.ToString(); - } - - return null; - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - try - { - var sourceBlob = blobContainer.GetBlockBlobReference(sourceFileName); - - var targetBlob = blobContainer.GetBlobReference(targetFileName); - - await targetBlob.StartCopyAsync(sourceBlob.Uri, null, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); - - while (targetBlob.CopyState.Status == CopyStatus.Pending) - { - ct.ThrowIfCancellationRequested(); - - await Task.Delay(50, ct); - await targetBlob.FetchAttributesAsync(null, null, null, ct); - } - - if (targetBlob.CopyState.Status != CopyStatus.Success) - { - throw new StorageException($"Copy of temporary file failed: {targetBlob.CopyState.Status}"); - } - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) - { - throw new AssetAlreadyExistsException(targetFileName); - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - var blob = blobContainer.GetBlockBlobReference(fileName); - - await blob.DownloadToStreamAsync(stream, null, null, null, ct); - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - var tempBlob = blobContainer.GetBlockBlobReference(fileName); - - await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); - } - catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - var blob = blobContainer.GetBlockBlobReference(fileName); - - return blob.DeleteIfExistsAsync(); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs deleted file mode 100644 index 27431204a..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.ObjectModel; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed partial class CosmosDbEventStore : DisposableObjectBase, IEventStore, IInitializable - { - private readonly DocumentClient documentClient; - private readonly Uri collectionUri; - private readonly Uri databaseUri; - private readonly string masterKey; - private readonly string databaseId; - private readonly JsonSerializerSettings serializerSettings; - - public JsonSerializerSettings SerializerSettings - { - get { return serializerSettings; } - } - - public string DatabaseId - { - get { return databaseId; } - } - - public string MasterKey - { - get { return masterKey; } - } - - public Uri ServiceUri - { - get { return documentClient.ServiceEndpoint; } - } - - public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, JsonSerializerSettings serializerSettings) - { - Guard.NotNull(documentClient, nameof(documentClient)); - Guard.NotNull(serializerSettings, nameof(serializerSettings)); - Guard.NotNullOrEmpty(masterKey, nameof(masterKey)); - Guard.NotNullOrEmpty(database, nameof(database)); - - this.documentClient = documentClient; - - databaseUri = UriFactory.CreateDatabaseUri(database); - databaseId = database; - - collectionUri = UriFactory.CreateDocumentCollectionUri(database, Constants.Collection); - - this.masterKey = masterKey; - - this.serializerSettings = serializerSettings; - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - documentClient.Dispose(); - } - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseId }); - - await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, - new DocumentCollection - { - PartitionKey = new PartitionKeyDefinition - { - Paths = new Collection - { - "/PartitionId" - } - }, - Id = Constants.LeaseCollection - }); - - await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, - new DocumentCollection - { - PartitionKey = new PartitionKeyDefinition - { - Paths = new Collection - { - "/eventStream" - } - }, - IndexingPolicy = new IndexingPolicy - { - IncludedPaths = new Collection - { - new IncludedPath - { - Path = "/*", - Indexes = new Collection - { - Index.Range(DataType.Number), - Index.Range(DataType.String) - } - } - } - }, - UniqueKeyPolicy = new UniqueKeyPolicy - { - UniqueKeys = new Collection - { - new UniqueKey - { - Paths = new Collection - { - "/eventStream", - "/eventStreamOffset" - } - } - } - }, - Id = Constants.Collection - }, - new RequestOptions - { - PartitionKey = new PartitionKey("/eventStream") - }); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs deleted file mode 100644 index ada171f2a..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// 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.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - public delegate bool EventPredicate(EventData data); - - public partial class CosmosDbEventStore : IEventStore, IInitializable - { - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) - { - Guard.NotNull(subscriber, nameof(subscriber)); - - ThrowIfDisposed(); - - return new CosmosDbSubscription(this, subscriber, streamFilter, position); - } - - public Task CreateIndexAsync(string property) - { - Guard.NotNullOrEmpty(property, nameof(property)); - - ThrowIfDisposed(); - - return TaskHelper.Done; - } - - public async Task> QueryAsync(string streamName, long streamPosition = 0) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - ThrowIfDisposed(); - - using (Profiler.TraceMethod()) - { - var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize); - - var result = new List(); - - await documentClient.QueryAsync(collectionUri, query, commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (eventStreamOffset >= streamPosition) - { - var eventData = @event.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); - } - } - - return TaskHelper.Done; - }); - - return result; - } - } - - public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - Guard.NotNullOrEmpty(property, nameof(property)); - Guard.NotNull(value, nameof(value)); - - ThrowIfDisposed(); - - StreamPosition lastPosition = position; - - var filterDefinition = FilterBuilder.CreateByProperty(property, value, lastPosition); - var filterExpression = FilterBuilder.CreateExpression(property, value); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - - ThrowIfDisposed(); - - StreamPosition lastPosition = position; - - var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition); - var filterExpression = FilterBuilder.CreateExpression(null, null); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - private async Task QueryAsync(Func callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default) - { - using (Profiler.TraceMethod()) - { - await documentClient.QueryAsync(collectionUri, query, async commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) - { - var eventData = @event.ToEventData(); - - if (filterExpression(eventData)) - { - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); - } - } - - commitOffset++; - } - }, ct); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs deleted file mode 100644 index 0c9186e15..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using NodaTime; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.EventSourcing -{ - public partial class CosmosDbEventStore - { - private const int MaxWriteAttempts = 20; - private const int MaxCommitSize = 10; - - public Task DeleteStreamAsync(string streamName) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - ThrowIfDisposed(); - - var query = FilterBuilder.AllIds(streamName); - - return documentClient.QueryAsync(collectionUri, query, commit => - { - var documentUri = UriFactory.CreateDocumentUri(databaseId, Constants.Collection, commit.Id.ToString()); - - return documentClient.DeleteDocumentAsync(documentUri); - }); - } - - public Task AppendAsync(Guid commitId, string streamName, ICollection events) - { - return AppendAsync(commitId, streamName, EtagVersion.Any, events); - } - - public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.NotEmpty(commitId, nameof(commitId)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); - - ThrowIfDisposed(); - - using (Profiler.TraceMethod()) - { - if (events.Count == 0) - { - return; - } - - var currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - - for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) - { - try - { - await documentClient.CreateDocumentAsync(collectionUri, commit); - - return; - } - catch (DocumentClientException ex) - { - if (ex.StatusCode == HttpStatusCode.Conflict) - { - currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - if (attempt < MaxWriteAttempts) - { - expectedVersion = currentVersion; - } - else - { - throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); - } - } - else - { - throw; - } - } - } - } - } - - private async Task GetEventStreamOffsetAsync(string streamName) - { - var query = - documentClient.CreateDocumentQuery(collectionUri, - FilterBuilder.LastPosition(streamName)); - - var document = await query.FirstOrDefaultAsync(); - - if (document != null) - { - return document.EventStreamOffset + document.EventsCount; - } - - return EtagVersion.Empty; - } - - private static CosmosDbEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - var commitEvents = new CosmosDbEvent[events.Count]; - - var i = 0; - - foreach (var e in events) - { - var mongoEvent = CosmosDbEvent.FromEventData(e); - - commitEvents[i++] = mongoEvent; - } - - var mongoCommit = new CosmosDbEventCommit - { - Id = commitId, - Events = commitEvents, - EventsCount = events.Count, - EventStream = streamName, - EventStreamOffset = expectedVersion, - Timestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeTicks() - }; - - return mongoCommit; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs deleted file mode 100644 index d2bb4b9b2..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs +++ /dev/null @@ -1,151 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing; -using Newtonsoft.Json; -using Squidex.Infrastructure.Tasks; -using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder; -using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo; -using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver - { - private readonly TaskCompletionSource processorStopRequested = new TaskCompletionSource(); - private readonly Task processorTask; - private readonly CosmosDbEventStore store; - private readonly Regex regex; - private readonly string hostName; - private readonly IEventSubscriber subscriber; - - public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string streamFilter, string position = null) - { - this.store = store; - - var fromBeginning = string.IsNullOrWhiteSpace(position); - - if (fromBeginning) - { - hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; - } - else - { - hostName = position; - } - - if (!StreamFilter.IsAll(streamFilter)) - { - regex = new Regex(streamFilter); - } - - this.subscriber = subscriber; - - processorTask = Task.Run(async () => - { - try - { - Collection CreateCollection(string name) - { - var collection = new Collection(); - - collection.CollectionName = name; - collection.DatabaseName = store.DatabaseId; - collection.MasterKey = store.MasterKey; - collection.Uri = store.ServiceUri; - - return collection; - } - - var processor = - await new Builder() - .WithFeedCollection(CreateCollection(Constants.Collection)) - .WithLeaseCollection(CreateCollection(Constants.LeaseCollection)) - .WithHostName(hostName) - .WithProcessorOptions(new Options { StartFromBeginning = fromBeginning, LeasePrefix = hostName }) - .WithObserverFactory(this) - .BuildAsync(); - - await processor.StartAsync(); - await processorStopRequested.Task; - await processor.StopAsync(); - } - catch (Exception ex) - { - await subscriber.OnErrorAsync(this, ex); - } - }); - } - - public IChangeFeedObserver CreateObserver() - { - return this; - } - - public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason) - { - if (reason == ChangeFeedObserverCloseReason.ObserverError) - { - await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed.")); - } - } - - public Task OpenAsync(IChangeFeedObserverContext context) - { - return TaskHelper.Done; - } - - public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) - { - if (!processorStopRequested.Task.IsCompleted) - { - foreach (var document in docs) - { - if (!processorStopRequested.Task.IsCompleted) - { - var streamName = document.GetPropertyValue("eventStream"); - - if (regex == null || regex.IsMatch(streamName)) - { - var commit = JsonConvert.DeserializeObject(document.ToString(), store.SerializerSettings); - - var eventStreamOffset = (int)commit.EventStreamOffset; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - var eventData = @event.ToEventData(); - - await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName, eventStreamOffset, eventData)); - } - } - } - } - } - } - - public void WakeUp() - { - } - - public Task StopAsync() - { - processorStopRequested.SetResult(true); - - return processorTask; - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs deleted file mode 100644 index 419248c2f..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs +++ /dev/null @@ -1,156 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.Azure.Documents; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal static class FilterBuilder - { - public static SqlQuerySpec AllIds(string streamName) - { - var query = - $"SELECT TOP 1 " + - $" e.id," + - $" e.eventsCount " + - $"FROM {Constants.Collection} e " + - $"WHERE " + - $" e.eventStream = @name " + - $"ORDER BY e.eventStreamOffset DESC"; - - var parameters = new SqlParameterCollection - { - new SqlParameter("@name", streamName) - }; - - return new SqlQuerySpec(query, parameters); - } - - public static SqlQuerySpec LastPosition(string streamName) - { - var query = - $"SELECT TOP 1 " + - $" e.eventStreamOffset," + - $" e.eventsCount " + - $"FROM {Constants.Collection} e " + - $"WHERE " + - $" e.eventStream = @name " + - $"ORDER BY e.eventStreamOffset DESC"; - - var parameters = new SqlParameterCollection - { - new SqlParameter("@name", streamName) - }; - - return new SqlQuerySpec(query, parameters); - } - - public static SqlQuerySpec ByStreamName(string streamName, long streamPosition = 0) - { - var query = - $"SELECT * " + - $"FROM {Constants.Collection} e " + - $"WHERE " + - $" e.eventStream = @name " + - $"AND e.eventStreamOffset >= @position " + - $"ORDER BY e.eventStreamOffset ASC"; - - var parameters = new SqlParameterCollection - { - new SqlParameter("@name", streamName), - new SqlParameter("@position", streamPosition) - }; - - return new SqlQuerySpec(query, parameters); - } - - public static SqlQuerySpec CreateByProperty(string property, object value, StreamPosition streamPosition) - { - var filters = new List(); - - var parameters = new SqlParameterCollection(); - - filters.ForPosition(parameters, streamPosition); - filters.ForProperty(parameters, property, value); - - return BuildQuery(filters, parameters); - } - - public static SqlQuerySpec CreateByFilter(string streamFilter, StreamPosition streamPosition) - { - var filters = new List(); - - var parameters = new SqlParameterCollection(); - - filters.ForPosition(parameters, streamPosition); - filters.ForRegex(parameters, streamFilter); - - return BuildQuery(filters, parameters); - } - - private static SqlQuerySpec BuildQuery(IEnumerable filters, SqlParameterCollection parameters) - { - var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp"; - - return new SqlQuerySpec(query, parameters); - } - - private static void ForProperty(this ICollection filters, SqlParameterCollection parameters, string property, object value) - { - filters.Add($"ARRAY_CONTAINS(e.events, {{ \"header\": {{ \"{property}\": @value }} }}, true)"); - - parameters.Add(new SqlParameter("@value", value)); - } - - private static void ForRegex(this ICollection filters, SqlParameterCollection parameters, string streamFilter) - { - if (!StreamFilter.IsAll(streamFilter)) - { - if (streamFilter.Contains("^")) - { - filters.Add($"STARTSWITH(e.eventStream, @filter)"); - } - else - { - filters.Add($"e.eventStream = @filter"); - } - - parameters.Add(new SqlParameter("@filter", streamFilter)); - } - } - - private static void ForPosition(this ICollection filters, SqlParameterCollection parameters, StreamPosition streamPosition) - { - if (streamPosition.IsEndOfCommit) - { - filters.Add($"e.timestamp > @time"); - } - else - { - filters.Add($"e.timestamp >= @time"); - } - - parameters.Add(new SqlParameter("@time", streamPosition.Timestamp)); - } - - public static EventPredicate CreateExpression(string property, object value) - { - if (!string.IsNullOrWhiteSpace(property)) - { - var jsonValue = JsonValue.Create(value); - - return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); - } - else - { - return x => true; - } - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs deleted file mode 100644 index c24e93ff1..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using Microsoft.Azure.Documents.Linq; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal static class FilterExtensions - { - public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) - { - var documentQuery = queryable.AsDocumentQuery(); - - using (documentQuery) - { - if (documentQuery.HasMoreResults) - { - var results = await documentQuery.ExecuteNextAsync(ct); - - return results.FirstOrDefault(); - } - } - - return default; - } - - public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) - { - var query = documentClient.CreateDocumentQuery(collectionUri, querySpec); - - return query.QueryAsync(handler, ct); - } - - public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default) - { - var documentQuery = queryable.AsDocumentQuery(); - - using (documentQuery) - { - while (documentQuery.HasMoreResults && !ct.IsCancellationRequested) - { - var items = await documentQuery.ExecuteNextAsync(ct); - - foreach (var item in items) - { - await handler(item); - } - } - } - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs deleted file mode 100644 index f0626ee5d..000000000 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class StreamPosition - { - public long Timestamp { get; } - - public long CommitOffset { get; } - - public long CommitSize { get; } - - public bool IsEndOfCommit - { - get { return CommitOffset == CommitSize - 1; } - } - - public StreamPosition(long timestamp, long commitOffset, long commitSize) - { - Timestamp = timestamp; - - CommitOffset = commitOffset; - CommitSize = commitSize; - } - - public static implicit operator string(StreamPosition position) - { - var parts = new object[] - { - position.Timestamp, - position.CommitOffset, - position.CommitSize - }; - - return string.Join("-", parts); - } - - public static implicit operator StreamPosition(string position) - { - if (!string.IsNullOrWhiteSpace(position)) - { - var parts = position.Split('-'); - - return new StreamPosition(long.Parse(parts[0]), long.Parse(parts[1]), long.Parse(parts[2])); - } - - return new StreamPosition(0, -1, -1); - } - } -} diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj deleted file mode 100644 index 3811eab8a..000000000 --- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs deleted file mode 100644 index 362972559..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class GetEventStoreHealthCheck : IHealthCheck - { - private readonly IEventStoreConnection connection; - - public GetEventStoreHealthCheck(IEventStoreConnection connection) - { - Guard.NotNull(connection, nameof(connection)); - - this.connection = connection; - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - await connection.ReadEventAsync("test", 1, false); - - return HealthCheckResult.Healthy("Application must query data from EventStore."); - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs deleted file mode 100644 index cc05285d8..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// 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 System.Text; -using EventStore.ClientAPI; -using Squidex.Infrastructure.Json; -using EventStoreData = EventStore.ClientAPI.EventData; - -namespace Squidex.Infrastructure.EventSourcing -{ - public static class Formatter - { - private static readonly HashSet PrivateHeaders = new HashSet { "$v", "$p", "$c", "$causedBy" }; - - public static StoredEvent Read(ResolvedEvent resolvedEvent, string prefix, IJsonSerializer serializer) - { - var @event = resolvedEvent.Event; - - var eventPayload = Encoding.UTF8.GetString(@event.Data); - var eventHeaders = GetHeaders(serializer, @event); - - var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); - - var streamName = GetStreamName(prefix, @event); - - return new StoredEvent( - streamName, - resolvedEvent.OriginalEventNumber.ToString(), - resolvedEvent.Event.EventNumber, - eventData); - } - - private static string GetStreamName(string prefix, RecordedEvent @event) - { - var streamName = @event.EventStreamId; - - if (streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - streamName = streamName.Substring(prefix.Length + 1); - } - - return streamName; - } - - private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event) - { - var headersJson = Encoding.UTF8.GetString(@event.Metadata); - var headers = serializer.Deserialize(headersJson); - - foreach (var key in headers.Keys.ToList()) - { - if (PrivateHeaders.Contains(key)) - { - headers.Remove(key); - } - } - - return headers; - } - - public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) - { - var payload = Encoding.UTF8.GetBytes(eventData.Payload); - - var headersJson = serializer.Serialize(eventData.Headers); - var headersBytes = Encoding.UTF8.GetBytes(headersJson); - - return new EventStoreData(Guid.NewGuid(), eventData.Type, true, payload, headersBytes); - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs deleted file mode 100644 index d06c1ea15..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ /dev/null @@ -1,224 +0,0 @@ -// ========================================================================== -// 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 System.Threading; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class GetEventStore : IEventStore, IInitializable - { - private const int WritePageSize = 500; - private const int ReadPageSize = 500; - private readonly IEventStoreConnection connection; - private readonly IJsonSerializer serializer; - private readonly string prefix; - private readonly ProjectionClient projectionClient; - - public GetEventStore(IEventStoreConnection connection, IJsonSerializer serializer, string prefix, string projectionHost) - { - Guard.NotNull(connection, nameof(connection)); - Guard.NotNull(serializer, nameof(serializer)); - - this.connection = connection; - this.serializer = serializer; - - this.prefix = prefix?.Trim(' ', '-').WithFallback("squidex"); - - projectionClient = new ProjectionClient(connection, prefix, projectionHost); - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - await connection.ConnectAsync(); - } - catch (Exception ex) - { - throw new ConfigurationException("Cannot connect to event store.", ex); - } - - await projectionClient.ConnectAsync(ct); - } - - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) - { - Guard.NotNull(streamFilter, nameof(streamFilter)); - - return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter); - } - - public Task CreateIndexAsync(string property) - { - Guard.NotNullOrEmpty(property, nameof(property)); - - return projectionClient.CreateProjectionAsync(property, string.Empty); - } - - public async Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - Guard.NotNullOrEmpty(property, nameof(property)); - Guard.NotNull(value, nameof(value)); - - using (Profiler.TraceMethod()) - { - var streamName = await projectionClient.CreateProjectionAsync(property, value); - - var sliceStart = projectionClient.ParsePosition(position); - - await QueryAsync(callback, streamName, sliceStart, ct); - } - } - - public async Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - - using (Profiler.TraceMethod()) - { - var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - - var sliceStart = projectionClient.ParsePosition(position); - - await QueryAsync(callback, streamName, sliceStart, ct); - } - } - - private async Task QueryAsync(Func callback, string streamName, long sliceStart, CancellationToken ct = default) - { - StreamEventsSlice currentSlice; - do - { - currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true); - - if (currentSlice.Status == SliceReadStatus.Success) - { - sliceStart = currentSlice.NextEventNumber; - - foreach (var resolved in currentSlice.Events) - { - var storedEvent = Formatter.Read(resolved, prefix, serializer); - - await callback(storedEvent); - } - } - } - while (!currentSlice.IsEndOfStream && !ct.IsCancellationRequested); - } - - public async Task> QueryAsync(string streamName, long streamPosition = 0) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - using (Profiler.TraceMethod()) - { - var result = new List(); - - var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start; - - StreamEventsSlice currentSlice; - do - { - currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true); - - if (currentSlice.Status == SliceReadStatus.Success) - { - sliceStart = currentSlice.NextEventNumber; - - foreach (var resolved in currentSlice.Events) - { - var storedEvent = Formatter.Read(resolved, prefix, serializer); - - result.Add(storedEvent); - } - } - } - while (!currentSlice.IsEndOfStream); - - return result; - } - } - - public Task DeleteStreamAsync(string streamName) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any); - } - - public Task AppendAsync(Guid commitId, string streamName, ICollection events) - { - return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); - } - - public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion)); - - return AppendEventsInternalAsync(streamName, expectedVersion, events); - } - - private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection events) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - - using (Profiler.TraceMethod(nameof(AppendAsync))) - { - if (events.Count == 0) - { - return; - } - - try - { - var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList(); - - if (eventsToSave.Count < WritePageSize) - { - await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave); - } - else - { - using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion)) - { - for (var p = 0; p < eventsToSave.Count; p += WritePageSize) - { - await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); - } - - await transaction.CommitAsync(); - } - } - } - catch (WrongExpectedVersionException ex) - { - throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion); - } - } - } - - private static int ParseVersion(string message) - { - return int.Parse(message.Substring(message.LastIndexOf(':') + 1)); - } - - private string GetStreamName(string streamName) - { - return $"{prefix}-{streamName}"; - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs deleted file mode 100644 index 0f06e4e77..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class GetEventStoreSubscription : IEventSubscription - { - private readonly IEventStoreConnection connection; - private readonly IEventSubscriber subscriber; - private readonly IJsonSerializer serializer; - private readonly string prefix; - private readonly EventStoreCatchUpSubscription subscription; - private readonly long? position; - - public GetEventStoreSubscription( - IEventStoreConnection connection, - IEventSubscriber subscriber, - IJsonSerializer serializer, - ProjectionClient projectionClient, - string position, - string prefix, - string streamFilter) - { - this.connection = connection; - - this.position = projectionClient.ParsePositionOrNull(position); - this.prefix = prefix; - - var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; - - this.serializer = serializer; - this.subscriber = subscriber; - - subscription = SubscribeToStream(streamName); - } - - public Task StopAsync() - { - subscription.Stop(); - - return TaskHelper.Done; - } - - public void WakeUp() - { - } - - private EventStoreCatchUpSubscription SubscribeToStream(string streamName) - { - var settings = CatchUpSubscriptionSettings.Default; - - return connection.SubscribeToStreamFrom(streamName, position, settings, - (s, e) => - { - var storedEvent = Formatter.Read(e, prefix, serializer); - - subscriber.OnEventAsync(this, storedEvent).Wait(); - }, null, - (s, reason, ex) => - { - if (reason != SubscriptionDropReason.ConnectionClosed && - reason != SubscriptionDropReason.UserInitiated) - { - ex = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); - - subscriber.OnErrorAsync(this, ex); - } - }); - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs deleted file mode 100644 index ca098bb1e..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs +++ /dev/null @@ -1,143 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; -using EventStore.ClientAPI.Projections; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class ProjectionClient - { - private readonly ConcurrentDictionary projections = new ConcurrentDictionary(); - private readonly IEventStoreConnection connection; - private readonly string prefix; - private readonly string projectionHost; - private ProjectionsManager projectionsManager; - - public ProjectionClient(IEventStoreConnection connection, string prefix, string projectionHost) - { - this.connection = connection; - - this.prefix = prefix; - this.projectionHost = projectionHost; - } - - private string CreateFilterProjectionName(string filter) - { - return $"by-{prefix.Slugify()}-{filter.Slugify()}"; - } - - private string CreatePropertyProjectionName(string property) - { - return $"by-{prefix.Slugify()}-{property.Slugify()}-property"; - } - - public async Task CreateProjectionAsync(string property, object value) - { - var name = CreatePropertyProjectionName(property); - - var query = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && e.metadata.{property}) {{ - linkTo('{name}-' + e.metadata.{property}, e); - }} - }} - }});"; - - await CreateProjectionAsync(name, query); - - return $"{name}-{value}"; - } - - public async Task CreateProjectionAsync(string streamFilter = null) - { - streamFilter = streamFilter ?? ".*"; - - var name = CreateFilterProjectionName(streamFilter); - - var query = - $@"fromAll() - .when({{ - $any: function (s, e) {{ - if (e.streamId.indexOf('{prefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({prefix.Length + 1}))) {{ - linkTo('{name}', e); - }} - }} - }});"; - - await CreateProjectionAsync(name, query); - - return name; - } - - private async Task CreateProjectionAsync(string name, string query) - { - if (projections.TryAdd(name, true)) - { - try - { - var credentials = connection.Settings.DefaultUserCredentials; - - await projectionsManager.CreateContinuousAsync(name, query, credentials); - } - catch (Exception ex) - { - if (!ex.Is()) - { - throw; - } - } - } - } - - public async Task ConnectAsync(CancellationToken ct = default) - { - var addressParts = projectionHost.Split(':'); - - if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port)) - { - port = 2113; - } - - var endpoints = await Dns.GetHostAddressesAsync(addressParts[0]); - var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), port); - - projectionsManager = - new ProjectionsManager( - connection.Settings.Log, endpoint, - connection.Settings.OperationTimeout); - try - { - await projectionsManager.ListAllAsync(connection.Settings.DefaultUserCredentials); - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); - } - } - - public long? ParsePositionOrNull(string position) - { - return long.TryParse(position, out var parsedPosition) ? (long?)parsedPosition : null; - } - - public long ParsePosition(string position) - { - return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start; - } - } -} diff --git a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj deleted file mode 100644 index 8e213b530..000000000 --- a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs deleted file mode 100644 index ab8207c8b..000000000 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Google; -using Google.Cloud.Storage.V1; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class GoogleCloudAssetStore : IAssetStore, IInitializable - { - private static readonly UploadObjectOptions IfNotExists = new UploadObjectOptions { IfGenerationMatch = 0 }; - private static readonly CopyObjectOptions IfNotExistsCopy = new CopyObjectOptions { IfGenerationMatch = 0 }; - private readonly string bucketName; - private StorageClient storageClient; - - public GoogleCloudAssetStore(string bucketName) - { - Guard.NotNullOrEmpty(bucketName, nameof(bucketName)); - - this.bucketName = bucketName; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - storageClient = StorageClient.Create(); - - await storageClient.GetBucketAsync(bucketName, cancellationToken: ct); - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot connect to google cloud bucket '${bucketName}'.", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - try - { - await storageClient.CopyObjectAsync(bucketName, sourceFileName, bucketName, targetFileName, IfNotExistsCopy, ct); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) - { - throw new AssetAlreadyExistsException(targetFileName); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - await storageClient.DownloadObjectAsync(bucketName, fileName, stream, cancellationToken: ct); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - await storageClient.UploadObjectAsync(bucketName, fileName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public async Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - try - { - await storageClient.DeleteObjectAsync(bucketName, fileName); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) - { - return; - } - } - } -} diff --git a/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj b/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj deleted file mode 100644 index 7151f2bde..000000000 --- a/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs deleted file mode 100644 index cde15c0da..000000000 --- a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.GridFS; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable - { - private const int BufferSize = 81920; - private readonly IGridFSBucket bucket; - - public MongoGridFsAssetStore(IGridFSBucket bucket) - { - Guard.NotNull(bucket, nameof(bucket)); - - this.bucket = bucket; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - await bucket.Database.ListCollectionsAsync(cancellationToken: ct); - } - catch (MongoException ex) - { - throw new ConfigurationException($"Cannot connect to Mongo GridFS bucket '${bucket.Options.BucketName}'.", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - try - { - var sourceName = GetFileName(sourceFileName, nameof(sourceFileName)); - - using (var readStream = await bucket.OpenDownloadStreamAsync(sourceFileName, cancellationToken: ct)) - { - await UploadAsync(targetFileName, readStream, false, ct); - } - } - catch (GridFSFileNotFoundException ex) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - try - { - var name = GetFileName(fileName, nameof(fileName)); - - using (var readStream = await bucket.OpenDownloadStreamAsync(name, cancellationToken: ct)) - { - await readStream.CopyToAsync(stream, BufferSize, ct); - } - } - catch (GridFSFileNotFoundException ex) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - try - { - var name = GetFileName(fileName, nameof(fileName)); - - if (overwrite) - { - await DeleteAsync(fileName); - } - - await bucket.UploadFromStreamAsync(fileName, fileName, stream, cancellationToken: ct); - } - catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - throw new AssetAlreadyExistsException(fileName); - } - catch (MongoBulkWriteException ex) when (ex.WriteErrors.Any(x => x.Category == ServerErrorCategory.DuplicateKey)) - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public async Task DeleteAsync(string fileName) - { - try - { - var name = GetFileName(fileName, nameof(fileName)); - - await bucket.DeleteAsync(name); - } - catch (GridFSFileNotFoundException) - { - return; - } - } - - private static string GetFileName(string fileName, string parameterName) - { - Guard.NotNullOrEmpty(fileName, parameterName); - - return fileName; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs deleted file mode 100644 index f4ce4efad..000000000 --- a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using MongoDB.Driver; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class MongoDBHealthCheck : IHealthCheck - { - private readonly IMongoDatabase mongoDatabase; - - public MongoDBHealthCheck(IMongoDatabase mongoDatabase) - { - Guard.NotNull(mongoDatabase, nameof(mongoDatabase)); - - this.mongoDatabase = mongoDatabase; - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); - - var result = await collectionNames.AnyAsync(cancellationToken); - - var status = result ? HealthStatus.Healthy : HealthStatus.Unhealthy; - - return new HealthCheckResult(status, "Application must query data from MongoDB"); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs deleted file mode 100644 index c51dcfd1e..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.EventSourcing -{ - public partial class MongoEventStore : MongoRepositoryBase, IEventStore - { - private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); - private static readonly FieldDefinition EventsCountField = Fields.Build(x => x.EventsCount); - private static readonly FieldDefinition EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); - private static readonly FieldDefinition EventStreamField = Fields.Build(x => x.EventStream); - private readonly IEventNotifier notifier; - - public IMongoCollection RawCollection - { - get { return Database.GetCollection(CollectionName()); } - } - - public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) - : base(database) - { - Guard.NotNull(notifier, nameof(notifier)); - - this.notifier = notifier; - } - - protected override string CollectionName() - { - return "Events"; - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { ReadPreference = ReadPreference.Primary, WriteConcern = WriteConcern.WMajority }; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.Timestamp) - .Ascending(x => x.EventStream)), - new CreateIndexModel( - Index - .Ascending(x => x.EventStream) - .Descending(x => x.EventStreamOffset), - new CreateIndexOptions - { - Unique = true - }) - }, ct); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs deleted file mode 100644 index 0c03ebbe0..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ========================================================================== -// 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.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using EventFilter = MongoDB.Driver.FilterDefinition; - -namespace Squidex.Infrastructure.EventSourcing -{ - public delegate bool EventPredicate(EventData data); - - public partial class MongoEventStore : MongoRepositoryBase, IEventStore - { - public Task CreateIndexAsync(string property) - { - Guard.NotNullOrEmpty(property, nameof(property)); - - return Collection.Indexes.CreateOneAsync( - new CreateIndexModel( - Index.Ascending(CreateIndexPath(property)))); - } - - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) - { - Guard.NotNull(subscriber, nameof(subscriber)); - - return new PollingSubscription(this, subscriber, streamFilter, position); - } - - public async Task> QueryAsync(string streamName, long streamPosition = 0) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - using (Profiler.TraceMethod()) - { - var commits = - await Collection.Find( - Filter.And( - Filter.Eq(EventStreamField, streamName), - Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize))) - .Sort(Sort.Ascending(TimestampField)).ToListAsync(); - - var result = new List(); - - foreach (var commit in commits) - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (eventStreamOffset >= streamPosition) - { - var eventData = @event.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); - } - } - } - - return result; - } - } - - public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - Guard.NotNullOrEmpty(property, nameof(property)); - Guard.NotNull(value, nameof(value)); - - StreamPosition lastPosition = position; - - var filterDefinition = CreateFilter(property, value, lastPosition); - var filterExpression = CreateFilterExpression(property, value); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) - { - Guard.NotNull(callback, nameof(callback)); - - StreamPosition lastPosition = position; - - var filterDefinition = CreateFilter(streamFilter, lastPosition); - var filterExpression = CreateFilterExpression(null, null); - - return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); - } - - private async Task QueryAsync(Func callback, StreamPosition lastPosition, EventFilter filterDefinition, EventPredicate filterExpression, CancellationToken ct = default) - { - using (Profiler.TraceMethod()) - { - await Collection.Find(filterDefinition, options: Batching.Options).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit => - { - var eventStreamOffset = (int)commit.EventStreamOffset; - - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; - - foreach (var @event in commit.Events) - { - eventStreamOffset++; - - if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) - { - var eventData = @event.ToEventData(); - - if (filterExpression(eventData)) - { - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - - await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); - } - } - - commitOffset++; - } - }, ct); - } - } - - private static EventFilter CreateFilter(string property, object value, StreamPosition streamPosition) - { - var filters = new List(); - - AppendByPosition(streamPosition, filters); - AppendByProperty(property, value, filters); - - return Filter.And(filters); - } - - private static EventFilter CreateFilter(string streamFilter, StreamPosition streamPosition) - { - var filters = new List(); - - AppendByPosition(streamPosition, filters); - AppendByStream(streamFilter, filters); - - return Filter.And(filters); - } - - private static void AppendByProperty(string property, object value, List filters) - { - filters.Add(Filter.Eq(CreateIndexPath(property), value)); - } - - private static void AppendByStream(string streamFilter, List filters) - { - if (!StreamFilter.IsAll(streamFilter)) - { - if (streamFilter.Contains("^")) - { - filters.Add(Filter.Regex(EventStreamField, streamFilter)); - } - else - { - filters.Add(Filter.Eq(EventStreamField, streamFilter)); - } - } - } - - private static void AppendByPosition(StreamPosition streamPosition, List filters) - { - if (streamPosition.IsEndOfCommit) - { - filters.Add(Filter.Gt(TimestampField, streamPosition.Timestamp)); - } - else - { - filters.Add(Filter.Gte(TimestampField, streamPosition.Timestamp)); - } - } - - private static EventPredicate CreateFilterExpression(string property, object value) - { - if (!string.IsNullOrWhiteSpace(property)) - { - var jsonValue = JsonValue.Create(value); - - return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); - } - else - { - return x => true; - } - } - - private static string CreateIndexPath(string property) - { - return $"Events.Metadata.{property}"; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs deleted file mode 100644 index a18a836d2..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.EventSourcing -{ - public partial class MongoEventStore - { - private const int MaxCommitSize = 10; - private const int MaxWriteAttempts = 20; - private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); - - public Task DeleteStreamAsync(string streamName) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - - return Collection.DeleteManyAsync(x => x.EventStream == streamName); - } - - public Task AppendAsync(Guid commitId, string streamName, ICollection events) - { - return AppendAsync(commitId, streamName, EtagVersion.Any, events); - } - - public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - Guard.NotEmpty(commitId, nameof(commitId)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); - Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - - using (Profiler.TraceMethod()) - { - if (events.Count == 0) - { - return; - } - - var currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - - for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) - { - try - { - await Collection.InsertOneAsync(commit); - - notifier.NotifyEventsStored(streamName); - - return; - } - catch (MongoWriteException ex) - { - if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) - { - currentVersion = await GetEventStreamOffsetAsync(streamName); - - if (expectedVersion > EtagVersion.Any) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - if (attempt < MaxWriteAttempts) - { - expectedVersion = currentVersion; - } - else - { - throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); - } - } - else - { - throw; - } - } - } - } - } - - private async Task GetEventStreamOffsetAsync(string streamName) - { - var document = - await Collection.Find(Filter.Eq(EventStreamField, streamName)) - .Project(Projection - .Include(EventStreamOffsetField) - .Include(EventsCountField)) - .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) - .FirstOrDefaultAsync(); - - if (document != null) - { - return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); - } - - return EtagVersion.Empty; - } - - private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) - { - var commitEvents = new MongoEvent[events.Count]; - - var i = 0; - - foreach (var e in events) - { - var mongoEvent = MongoEvent.FromEventData(e); - - commitEvents[i++] = mongoEvent; - } - - var mongoCommit = new MongoEventCommit - { - Id = commitId, - Events = commitEvents, - EventsCount = events.Count, - EventStream = streamName, - EventStreamOffset = expectedVersion, - Timestamp = EmptyTimestamp - }; - - return mongoCommit; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs deleted file mode 100644 index 406abd90a..000000000 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; - -namespace Squidex.Infrastructure.EventSourcing -{ - internal sealed class StreamPosition - { - private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(946681200, 0); - - public BsonTimestamp Timestamp { get; } - - public long CommitOffset { get; } - - public long CommitSize { get; } - - public bool IsEndOfCommit - { - get { return CommitOffset == CommitSize - 1; } - } - - public StreamPosition(BsonTimestamp timestamp, long commitOffset, long commitSize) - { - Timestamp = timestamp; - - CommitOffset = commitOffset; - CommitSize = commitSize; - } - - public static implicit operator string(StreamPosition position) - { - var parts = new object[] - { - position.Timestamp.Timestamp, - position.Timestamp.Increment, - position.CommitOffset, - position.CommitSize - }; - - return string.Join("-", parts); - } - - public static implicit operator StreamPosition(string position) - { - if (!string.IsNullOrWhiteSpace(position)) - { - var parts = position.Split('-'); - - return new StreamPosition(new BsonTimestamp(int.Parse(parts[0]), int.Parse(parts[1])), long.Parse(parts[2]), long.Parse(parts[3])); - } - - return new StreamPosition(EmptyTimestamp, -1, -1); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs deleted file mode 100644 index 7fe870e3c..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Reflection; -using System.Threading; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Conventions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Squidex.Infrastructure.MongoDb -{ - public static class BsonJsonConvention - { - private static volatile int isRegistered; - - public static void Register(JsonSerializer serializer) - { - if (Interlocked.Increment(ref isRegistered) == 1) - { - var pack = new ConventionPack(); - - pack.AddMemberMapConvention("JsonBson", memberMap => - { - var attributes = memberMap.MemberInfo.GetCustomAttributes(); - - if (attributes.OfType().Any()) - { - var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); - var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); - - memberMap.SetSerializer((IBsonSerializer)bsonSerializer); - } - else if (memberMap.MemberType == typeof(JToken)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - else if (memberMap.MemberType == typeof(JObject)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - else if (memberMap.MemberType == typeof(JValue)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - }); - - ConventionRegistry.Register("json", pack, t => true); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs deleted file mode 100644 index f801aeddc..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.IO; -using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader; -using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonReader : NewtonsoftJsonReader - { - private readonly IBsonReader bsonReader; - - public BsonJsonReader(IBsonReader bsonReader) - { - Guard.NotNull(bsonReader, nameof(bsonReader)); - - this.bsonReader = bsonReader; - } - - public override bool Read() - { - if (bsonReader.State == BsonReaderState.Initial || - bsonReader.State == BsonReaderState.ScopeDocument || - bsonReader.State == BsonReaderState.Type) - { - bsonReader.ReadBsonType(); - } - - if (bsonReader.State == BsonReaderState.Name) - { - SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson()); - } - else if (bsonReader.State == BsonReaderState.Value) - { - switch (bsonReader.CurrentBsonType) - { - case BsonType.Document: - SetToken(NewtonsoftJsonToken.StartObject); - bsonReader.ReadStartDocument(); - break; - case BsonType.Array: - SetToken(NewtonsoftJsonToken.StartArray); - bsonReader.ReadStartArray(); - break; - case BsonType.Undefined: - SetToken(NewtonsoftJsonToken.Undefined); - bsonReader.ReadUndefined(); - break; - case BsonType.Null: - SetToken(NewtonsoftJsonToken.Null); - bsonReader.ReadNull(); - break; - case BsonType.String: - SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString()); - break; - case BsonType.Binary: - SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes); - break; - case BsonType.Boolean: - SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean()); - break; - case BsonType.DateTime: - SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime()); - break; - case BsonType.Int32: - SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32()); - break; - case BsonType.Int64: - SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64()); - break; - case BsonType.Double: - SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble()); - break; - case BsonType.Decimal128: - SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128())); - break; - default: - throw new NotSupportedException(); - } - } - else if (bsonReader.State == BsonReaderState.EndOfDocument) - { - SetToken(NewtonsoftJsonToken.EndObject); - bsonReader.ReadEndDocument(); - } - else if (bsonReader.State == BsonReaderState.EndOfArray) - { - SetToken(NewtonsoftJsonToken.EndArray); - bsonReader.ReadEndArray(); - } - - if (bsonReader.State == BsonReaderState.Initial) - { - return true; - } - - return !bsonReader.IsAtEndOfFile(); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs deleted file mode 100644 index eefb3e3e5..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonSerializer : ClassSerializerBase where T : class - { - private readonly JsonSerializer serializer; - - public BsonJsonSerializer(JsonSerializer serializer) - { - Guard.NotNull(serializer, nameof(serializer)); - - this.serializer = serializer; - } - - public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - var bsonReader = context.Reader; - - if (bsonReader.GetCurrentBsonType() == BsonType.Null) - { - bsonReader.ReadNull(); - - return null; - } - else - { - var jsonReader = new BsonJsonReader(bsonReader); - - return serializer.Deserialize(jsonReader); - } - } - - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value) - { - var bsonWriter = context.Writer; - - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - var jsonWriter = new BsonJsonWriter(bsonWriter); - - serializer.Serialize(jsonWriter, value); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs deleted file mode 100644 index 558970951..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Globalization; -using MongoDB.Bson.IO; -using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonWriter : NewtonsoftJSonWriter - { - private readonly IBsonWriter bsonWriter; - - public BsonJsonWriter(IBsonWriter bsonWriter) - { - Guard.NotNull(bsonWriter, nameof(bsonWriter)); - - this.bsonWriter = bsonWriter; - } - - public override void WritePropertyName(string name) - { - bsonWriter.WriteName(name.EscapeJson()); - } - - public override void WritePropertyName(string name, bool escape) - { - bsonWriter.WriteName(name.EscapeJson()); - } - - public override void WriteStartArray() - { - bsonWriter.WriteStartArray(); - } - - public override void WriteEndArray() - { - bsonWriter.WriteEndArray(); - } - - public override void WriteStartObject() - { - bsonWriter.WriteStartDocument(); - } - - public override void WriteEndObject() - { - bsonWriter.WriteEndDocument(); - } - - public override void WriteNull() - { - bsonWriter.WriteNull(); - } - - public override void WriteUndefined() - { - bsonWriter.WriteUndefined(); - } - - public override void WriteValue(string value) - { - bsonWriter.WriteString(value); - } - - public override void WriteValue(int value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(uint value) - { - bsonWriter.WriteInt32((int)value); - } - - public override void WriteValue(long value) - { - bsonWriter.WriteInt64(value); - } - - public override void WriteValue(ulong value) - { - bsonWriter.WriteInt64((long)value); - } - - public override void WriteValue(float value) - { - bsonWriter.WriteDouble(value); - } - - public override void WriteValue(double value) - { - bsonWriter.WriteDouble(value); - } - - public override void WriteValue(bool value) - { - bsonWriter.WriteBoolean(value); - } - - public override void WriteValue(short value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(ushort value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(char value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(byte value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(sbyte value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(decimal value) - { - bsonWriter.WriteDecimal128(value); - } - - public override void WriteValue(DateTime value) - { - bsonWriter.WriteString(value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - } - - public override void WriteValue(DateTimeOffset value) - { - if (value.Offset == TimeSpan.Zero) - { - bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - } - else - { - bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - } - } - - public override void WriteValue(byte[] value) - { - bsonWriter.WriteBytes(value); - } - - public override void WriteValue(TimeSpan value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void WriteValue(Guid value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void WriteValue(Uri value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void Flush() - { - bsonWriter.Flush(); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs deleted file mode 100644 index 755a4ae25..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/JTokenSerializer.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using Newtonsoft.Json.Linq; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class JTokenSerializer : ClassSerializerBase where T : JToken - { - public static readonly JTokenSerializer Instance = new JTokenSerializer(); - - public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - var bsonReader = context.Reader; - - if (bsonReader.GetCurrentBsonType() == BsonType.Null) - { - bsonReader.ReadNull(); - - return null; - } - else - { - var jsonReader = new BsonJsonReader(bsonReader); - - return (T)JToken.ReadFrom(jsonReader); - } - } - - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value) - { - var bsonWriter = context.Writer; - - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - var jsonWriter = new BsonJsonWriter(bsonWriter); - - value.WriteTo(jsonWriter); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs deleted file mode 100644 index 0dc8cbf39..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ /dev/null @@ -1,216 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.States; - -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - -namespace Squidex.Infrastructure.MongoDb -{ - public static class MongoExtensions - { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - - public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName) - { - var options = new ListCollectionNamesOptions - { - Filter = new BsonDocument("name", collectionName) - }; - - return (await database.ListCollectionNamesAsync(options)).Any(); - } - - public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document) - { - try - { - await collection.InsertOneAsync(document); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - return false; - } - - throw; - } - - return true; - } - - public static async Task TryDropOneAsync(this IMongoIndexManager indexes, string name) - { - try - { - await indexes.DropOneAsync(name); - } - catch - { - /* NOOP */ - } - } - - public static IFindFluent Only(this IFindFluent find, - Expression> include) - { - return find.Project(Builders.Projection.Include(include)); - } - - public static IFindFluent Only(this IFindFluent find, - Expression> include1, - Expression> include2) - { - return find.Project(Builders.Projection.Include(include1).Include(include2)); - } - - public static IFindFluent Only(this IFindFluent find, - Expression> include1, - Expression> include2, - Expression> include3) - { - return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); - } - - public static IFindFluent Not(this IFindFluent find, - Expression> exclude) - { - return find.Project(Builders.Projection.Exclude(exclude)); - } - - public static IFindFluent Not(this IFindFluent find, - Expression> exclude1, - Expression> exclude2) - { - return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2)); - } - - public static IFindFluent Not(this IFindFluent find, - Expression> exclude1, - Expression> exclude2, - Expression> exclude3) - { - return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2).Exclude(exclude3)); - } - - public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, Func, UpdateDefinition> updater) where T : IVersionedEntity - { - try - { - var update = updater(Builders.Update.Set(x => x.Version, newVersion)); - - await collection.UpdateOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, update, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - - public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, T doc) where T : IVersionedEntity - { - try - { - await collection.ReplaceOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, doc, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await collection.Find(x => x.Id.Equals(key)).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion[nameof(IVersionedEntity.Version)].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - - public static async Task ForEachPipelineAsync(this IAsyncCursorSource source, Func processor, CancellationToken cancellationToken = default) - { - using (var cursor = await source.ToCursorAsync(cancellationToken)) - { - await cursor.ForEachPipelineAsync(processor, cancellationToken); - } - } - - public static async Task ForEachPipelineAsync(this IAsyncCursor source, Func processor, CancellationToken cancellationToken = default) - { - using (var selfToken = new CancellationTokenSource()) - { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(selfToken.Token, cancellationToken)) - { - var actionBlock = - new ActionBlock(async x => - { - if (!combined.IsCancellationRequested) - { - await processor(x); - } - }, - new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - BoundedCapacity = Batching.BufferSize - }); - try - { - await source.ForEachAsync(async i => - { - var t = source; - - if (!await actionBlock.SendAsync(i, combined.Token)) - { - selfToken.Cancel(); - } - }, combined.Token); - - actionBlock.Complete(); - } - catch (Exception ex) - { - ((IDataflowBlock)actionBlock).Fault(ex); - } - finally - { - await actionBlock.Completion; - } - } - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs deleted file mode 100644 index afe2cf5f4..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.Tasks; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure.MongoDb -{ - public abstract class MongoRepositoryBase : IInitializable - { - private const string CollectionFormat = "{0}Set"; - - protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - protected static readonly SortDefinitionBuilder Sort = Builders.Sort; - protected static readonly UpdateDefinitionBuilder Update = Builders.Update; - protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; - protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; - protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; - protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; - - private readonly IMongoDatabase mongoDatabase; - private Lazy> mongoCollection; - - protected IMongoCollection Collection - { - get { return mongoCollection.Value; } - } - - protected IMongoDatabase Database - { - get { return mongoDatabase; } - } - - static MongoRepositoryBase() - { - RefTokenSerializer.Register(); - - InstantSerializer.Register(); - } - - protected MongoRepositoryBase(IMongoDatabase database) - { - Guard.NotNull(database, nameof(database)); - - mongoDatabase = database; - mongoCollection = CreateCollection(); - } - - protected virtual MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings(); - } - - protected virtual string CollectionName() - { - return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name); - } - - private Lazy> CreateCollection() - { - return new Lazy>(() => - mongoDatabase.GetCollection( - CollectionName(), - CollectionSettings() ?? new MongoCollectionSettings())); - } - - protected virtual Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return TaskHelper.Done; - } - - public virtual async Task ClearAsync() - { - await Database.DropCollectionAsync(CollectionName()); - - await SetupCollectionAsync(Collection); - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - try - { - await SetupCollectionAsync(Collection, ct); - } - catch (Exception ex) - { - throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs deleted file mode 100644 index b89b8d028..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Driver; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure.MongoDb.Queries -{ - public static class FilterBuilder - { - public static (FilterDefinition Filter, bool Last) BuildFilter(this ClrQuery query, bool supportsSearch = true) - { - if (query.FullText != null) - { - if (!supportsSearch) - { - throw new ValidationException("Query $search clause not supported."); - } - - return (Builders.Filter.Text(query.FullText), false); - } - - if (query.Filter != null) - { - return (query.Filter.BuildFilter(), true); - } - - return (null, false); - } - - public static FilterDefinition BuildFilter(this FilterNode filterNode) - { - return FilterVisitor.Visit(filterNode); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs deleted file mode 100644 index c5a597b41..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Linq; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Infrastructure.MongoDb.Queries -{ - public sealed class FilterVisitor : FilterNodeVisitor, ClrValue> - { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - private static readonly FilterVisitor Instance = new FilterVisitor(); - - private FilterVisitor() - { - } - - public static FilterDefinition Visit(FilterNode node) - { - return node.Accept(Instance); - } - - public override FilterDefinition Visit(NegateFilter nodeIn) - { - return Filter.Not(nodeIn.Filter.Accept(this)); - } - - public override FilterDefinition Visit(LogicalFilter nodeIn) - { - if (nodeIn.Type == LogicalFilterType.And) - { - return Filter.And(nodeIn.Filters.Select(x => x.Accept(this))); - } - else - { - return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this))); - } - } - - public override FilterDefinition Visit(CompareFilter nodeIn) - { - var propertyName = nodeIn.Path.ToString(); - - var value = nodeIn.Value.Value; - - switch (nodeIn.Operator) - { - case CompareOperator.Empty: - return Filter.Or( - Filter.Exists(propertyName, false), - Filter.Eq(propertyName, default(T)), - Filter.Eq(propertyName, string.Empty), - Filter.Eq(propertyName, new T[0])); - case CompareOperator.StartsWith: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); - case CompareOperator.Contains: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); - case CompareOperator.EndsWith: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); - case CompareOperator.Equals: - return Filter.Eq(propertyName, value); - case CompareOperator.GreaterThan: - return Filter.Gt(propertyName, value); - case CompareOperator.GreaterThanOrEqual: - return Filter.Gte(propertyName, value); - case CompareOperator.LessThan: - return Filter.Lt(propertyName, value); - case CompareOperator.LessThanOrEqual: - return Filter.Lte(propertyName, value); - case CompareOperator.NotEquals: - return Filter.Ne(propertyName, value); - case CompareOperator.In: - return Filter.In(propertyName, ((IList)value).OfType()); - } - - throw new NotSupportedException(); - } - - private static BsonRegularExpression BuildRegex(CompareFilter node, Func formatter) - { - return new BsonRegularExpression(formatter(node.Value.Value.ToString()), "i"); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs deleted file mode 100644 index 68b087166..000000000 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using MongoDB.Driver; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Infrastructure.MongoDb.Queries -{ - public static class SortBuilder - { - public static SortDefinition BuildSort(this ClrQuery query) - { - if (query.Sort.Count > 0) - { - var sorts = new List>(); - - foreach (var sort in query.Sort) - { - sorts.Add(OrderBy(sort)); - } - - if (sorts.Count > 1) - { - return Builders.Sort.Combine(sorts); - } - else - { - return sorts[0]; - } - } - - return null; - } - - public static SortDefinition OrderBy(SortNode sort) - { - var propertyName = string.Join(".", sort.Path); - - if (sort.Order == SortOrder.Ascending) - { - return Builders.Sort.Ascending(propertyName); - } - else - { - return Builders.Sort.Descending(propertyName); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj deleted file mode 100644 index 3ce977368..000000000 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs deleted file mode 100644 index e9a7cc675..000000000 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Newtonsoft.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.States -{ - public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore - { - public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - BsonJsonConvention.Register(jsonSerializer); - } - - protected override string CollectionName() - { - var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); - - var name = attribute?.Name ?? typeof(T).Name; - - return $"States_{name}"; - } - - public async Task<(T Value, long Version)> ReadAsync(TKey key) - { - using (Profiler.TraceMethod>()) - { - var existing = - await Collection.Find(x => x.Id.Equals(key)) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.Doc, existing.Version); - } - - return (default, EtagVersion.NotFound); - } - } - - public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion) - { - using (Profiler.TraceMethod>()) - { - await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); - } - } - - public async Task ReadAllAsync(Func callback, CancellationToken ct = default) - { - using (Profiler.TraceMethod>()) - { - await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(x.Doc, x.Version), ct); - } - } - - public async Task RemoveAsync(TKey key) - { - using (Profiler.TraceMethod>()) - { - await Collection.DeleteOneAsync(x => x.Id.Equals(key)); - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs deleted file mode 100644 index 421862d29..000000000 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// 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 System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class MongoUsageRepository : MongoRepositoryBase, IUsageRepository - { - private static readonly BulkWriteOptions Unordered = new BulkWriteOptions { IsOrdered = false }; - - public MongoUsageRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "UsagesV2"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - return collection.Indexes.CreateOneAsync( - new CreateIndexModel( - Index - .Ascending(x => x.Key) - .Ascending(x => x.Category) - .Ascending(x => x.Date)), - cancellationToken: ct); - } - - public async Task TrackUsagesAsync(UsageUpdate update) - { - Guard.NotNull(update, nameof(update)); - - if (update.Counters.Count > 0) - { - var (filter, updateStatement) = CreateOperation(update); - - await Collection.UpdateOneAsync(filter, updateStatement, Upsert); - } - } - - public async Task TrackUsagesAsync(params UsageUpdate[] updates) - { - if (updates.Length == 1) - { - await TrackUsagesAsync(updates[0]); - } - else if (updates.Length > 0) - { - var writes = new List>(); - - foreach (var update in updates) - { - if (update.Counters.Count > 0) - { - var (filter, updateStatement) = CreateOperation(update); - - writes.Add(new UpdateOneModel(filter, updateStatement) { IsUpsert = true }); - } - } - - await Collection.BulkWriteAsync(writes, Unordered); - } - } - - private static (FilterDefinition, UpdateDefinition) CreateOperation(UsageUpdate usageUpdate) - { - var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; - - var update = Update - .SetOnInsert(x => x.Key, usageUpdate.Key) - .SetOnInsert(x => x.Date, usageUpdate.Date) - .SetOnInsert(x => x.Category, usageUpdate.Category); - - foreach (var counter in usageUpdate.Counters) - { - update = update.Inc($"Counters.{counter.Key}", counter.Value); - } - - var filter = Filter.Eq(x => x.Id, id); - - return (filter, update); - } - - public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); - - return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); - } - } -} diff --git a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs deleted file mode 100644 index 5e426c8fc..000000000 --- a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer - { - private readonly IJsonSerializer jsonSerializer; - private readonly string eventPublisherName; - private readonly string exchange; - private readonly string eventsFilter; - private readonly ConnectionFactory connectionFactory; - private readonly Lazy connection; - private readonly Lazy channel; - - public string Name - { - get { return eventPublisherName; } - } - - public string EventsFilter - { - get { return eventsFilter; } - } - - public RabbitMqEventConsumer(IJsonSerializer jsonSerializer, string eventPublisherName, string uri, string exchange, string eventsFilter) - { - Guard.NotNullOrEmpty(uri, nameof(uri)); - Guard.NotNullOrEmpty(eventPublisherName, nameof(eventPublisherName)); - Guard.NotNullOrEmpty(exchange, nameof(exchange)); - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; - connection = new Lazy(connectionFactory.CreateConnection); - channel = new Lazy(connection.Value.CreateModel); - - this.exchange = exchange; - this.eventsFilter = eventsFilter; - this.eventPublisherName = eventPublisherName; - this.jsonSerializer = jsonSerializer; - } - - protected override void DisposeObject(bool disposing) - { - if (connection.IsValueCreated) - { - connection.Value.Close(); - connection.Value.Dispose(); - } - } - - public Task InitializeAsync(CancellationToken ct = default) - { - try - { - var currentConnection = connection.Value; - - if (!currentConnection.IsOpen) - { - throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}"); - } - - return TaskHelper.Done; - } - catch (Exception e) - { - throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}", e); - } - } - - public bool Handles(StoredEvent @event) - { - return true; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public Task On(Envelope @event) - { - var jsonString = jsonSerializer.Serialize(@event); - var jsonBytes = Encoding.UTF8.GetBytes(jsonString); - - channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj deleted file mode 100644 index 53db2cb03..000000000 --- a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj deleted file mode 100644 index 5cf748e0a..000000000 --- a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - Squidex.Infrastructure - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs b/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs deleted file mode 100644 index ddf8465e0..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.Assets -{ - [Serializable] - public class AssetAlreadyExistsException : Exception - { - public AssetAlreadyExistsException(string fileName) - : base(FormatMessage(fileName)) - { - } - - public AssetAlreadyExistsException(string fileName, Exception inner) - : base(FormatMessage(fileName), inner) - { - } - - protected AssetAlreadyExistsException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - private static string FormatMessage(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - return $"An asset with name '{fileName}' already exists."; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetFile.cs b/src/Squidex.Infrastructure/Assets/AssetFile.cs deleted file mode 100644 index 4f5ef010f..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetFile.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class AssetFile - { - private readonly Func openAction; - - public string FileName { get; } - - public string MimeType { get; } - - public long FileSize { get; } - - public AssetFile(string fileName, string mimeType, long fileSize, Func openAction) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); - Guard.GreaterEquals(fileSize, 0, nameof(fileSize)); - - FileName = fileName; - FileSize = fileSize; - - MimeType = mimeType; - - this.openAction = openAction; - } - - public Stream OpenRead() - { - return openAction(); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs b/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs deleted file mode 100644 index 1691a8bbf..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.Assets -{ - [Serializable] - public class AssetNotFoundException : Exception - { - public AssetNotFoundException(string fileName) - : base(FormatMessage(fileName)) - { - } - - public AssetNotFoundException(string fileName, Exception inner) - : base(FormatMessage(fileName), inner) - { - } - - protected AssetNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - private static string FormatMessage(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - return $"An asset with name '{fileName}' does not exist."; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs b/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs deleted file mode 100644 index a8824c314..000000000 --- a/src/Squidex.Infrastructure/Assets/AssetStoreExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public static class AssetStoreExtensions - { - public static string GeneratePublicUrl(this IAssetStore store, Guid id, long version, string suffix) - { - return store.GeneratePublicUrl(id.ToString(), version, suffix); - } - - public static string GeneratePublicUrl(this IAssetStore store, string id, long version, string suffix) - { - return store.GeneratePublicUrl(GetFileName(id, version, suffix)); - } - - public static Task CopyAsync(this IAssetStore store, string sourceFileName, Guid id, long version, string suffix, CancellationToken ct = default) - { - return store.CopyAsync(sourceFileName, id.ToString(), version, suffix, ct); - } - - public static Task CopyAsync(this IAssetStore store, string sourceFileName, string id, long version, string suffix, CancellationToken ct = default) - { - return store.CopyAsync(sourceFileName, GetFileName(id, version, suffix), ct); - } - - public static Task DownloadAsync(this IAssetStore store, Guid id, long version, string suffix, Stream stream, CancellationToken ct = default) - { - return store.DownloadAsync(id.ToString(), version, suffix, stream, ct); - } - - public static Task DownloadAsync(this IAssetStore store, string id, long version, string suffix, Stream stream, CancellationToken ct = default) - { - return store.DownloadAsync(GetFileName(id, version, suffix), stream, ct); - } - - public static Task UploadAsync(this IAssetStore store, Guid id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - return store.UploadAsync(id.ToString(), version, suffix, stream, overwrite, ct); - } - - public static Task UploadAsync(this IAssetStore store, string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - return store.UploadAsync(GetFileName(id, version, suffix), stream, overwrite, ct); - } - - public static Task DeleteAsync(this IAssetStore store, Guid id, long version, string suffix) - { - return store.DeleteAsync(id.ToString(), version, suffix); - } - - public static Task DeleteAsync(this IAssetStore store, string id, long version, string suffix) - { - return store.DeleteAsync(GetFileName(id, version, suffix)); - } - - public static string GetFileName(string id, long version, string suffix = null) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs b/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs deleted file mode 100644 index 0c417fe83..000000000 --- a/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FluentFTP; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class FTPAssetStore : IAssetStore, IInitializable - { - private readonly string path; - private readonly ISemanticLog log; - private readonly Func factory; - - public FTPAssetStore(Func factory, string path, ISemanticLog log) - { - Guard.NotNull(factory, nameof(factory)); - Guard.NotNullOrEmpty(path, nameof(path)); - Guard.NotNull(log, nameof(log)); - - this.factory = factory; - this.path = path; - - this.log = log; - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public async Task InitializeAsync(CancellationToken ct = default) - { - using (var client = factory()) - { - await client.ConnectAsync(ct); - - if (!await client.DirectoryExistsAsync(path, ct)) - { - await client.CreateDirectoryAsync(path, ct); - } - } - - log.LogInformation(w => w - .WriteProperty("action", "FTPAssetStoreConfigured") - .WriteProperty("path", path)); - } - - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - using (var client = GetFtpClient()) - { - var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); - - using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) - { - await DownloadAsync(client, sourceFileName, stream, ct); - await UploadAsync(client, targetFileName, stream, false, ct); - } - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - using (var client = GetFtpClient()) - { - await DownloadAsync(client, fileName, stream, ct); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - using (var client = GetFtpClient()) - { - await UploadAsync(client, fileName, stream, overwrite, ct); - } - } - - private static async Task DownloadAsync(IFtpClient client, string fileName, Stream stream, CancellationToken ct) - { - try - { - await client.DownloadAsync(stream, fileName, token: ct); - } - catch (FtpException ex) when (IsNotFound(ex)) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct) - { - if (!overwrite && await client.FileExistsAsync(fileName, ct)) - { - throw new AssetAlreadyExistsException(fileName); - } - - await client.UploadAsync(stream, fileName, overwrite ? FtpExists.Overwrite : FtpExists.Skip, true, null, ct); - } - - public async Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - using (var client = GetFtpClient()) - { - try - { - await client.DeleteFileAsync(fileName); - } - catch (FtpException ex) - { - if (!IsNotFound(ex)) - { - throw; - } - } - } - } - - private IFtpClient GetFtpClient() - { - var client = factory(); - - client.Connect(); - client.SetWorkingDirectory(path); - - return client; - } - - private static bool IsNotFound(Exception exception) - { - if (exception is FtpCommandException command) - { - return command.CompletionCode == "550"; - } - - return exception.InnerException != null && IsNotFound(exception.InnerException); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs deleted file mode 100644 index c9a3e83f0..000000000 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class FolderAssetStore : IAssetStore, IInitializable - { - private const int BufferSize = 81920; - private readonly ISemanticLog log; - private readonly DirectoryInfo directory; - - public FolderAssetStore(string path, ISemanticLog log) - { - Guard.NotNullOrEmpty(path, nameof(path)); - Guard.NotNull(log, nameof(log)); - - this.log = log; - - directory = new DirectoryInfo(path); - } - - public Task InitializeAsync(CancellationToken ct = default) - { - try - { - if (!directory.Exists) - { - directory.Create(); - } - - log.LogInformation(w => w - .WriteProperty("action", "FolderAssetStoreConfigured") - .WriteProperty("path", directory.FullName)); - - return TaskHelper.Done; - } - catch (Exception ex) - { - throw new ConfigurationException($"Cannot access directory {directory.FullName}", ex); - } - } - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - var targetFile = GetFile(targetFileName); - var sourceFile = GetFile(sourceFileName); - - try - { - sourceFile.CopyTo(targetFile.FullName); - - return TaskHelper.Done; - } - catch (IOException) when (targetFile.Exists) - { - throw new AssetAlreadyExistsException(targetFileName); - } - catch (FileNotFoundException ex) - { - throw new AssetNotFoundException(sourceFileName, ex); - } - } - - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - var file = GetFile(fileName); - - try - { - using (var fileStream = file.OpenRead()) - { - await fileStream.CopyToAsync(stream, BufferSize, ct); - } - } - catch (FileNotFoundException ex) - { - throw new AssetNotFoundException(fileName, ex); - } - } - - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNull(stream, nameof(stream)); - - var file = GetFile(fileName); - - try - { - using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write)) - { - await stream.CopyToAsync(fileStream, BufferSize, ct); - } - } - catch (IOException) when (file.Exists) - { - throw new AssetAlreadyExistsException(file.Name); - } - } - - public Task DeleteAsync(string fileName) - { - var file = GetFile(fileName); - - file.Delete(); - - return TaskHelper.Done; - } - - private FileInfo GetFile(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - return new FileInfo(GetPath(fileName)); - } - - private string GetPath(string name) - { - return Path.Combine(directory.FullName, name); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/HasherStream.cs b/src/Squidex.Infrastructure/Assets/HasherStream.cs deleted file mode 100644 index ea11e8682..000000000 --- a/src/Squidex.Infrastructure/Assets/HasherStream.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Security.Cryptography; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class HasherStream : Stream - { - private readonly Stream inner; - private readonly IncrementalHash hasher; - - public override bool CanRead - { - get { return inner.CanRead; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { return inner.Length; } - } - - public override long Position - { - get { return inner.Position; } - set { throw new NotSupportedException(); } - } - - public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName) - { - Guard.NotNull(inner, nameof(inner)); - - this.inner = inner; - - hasher = IncrementalHash.CreateHash(hashAlgorithmName); - } - - public override int Read(byte[] buffer, int offset, int count) - { - var read = inner.Read(buffer, offset, count); - - if (read > 0) - { - hasher.AppendData(buffer, offset, read); - } - - return read; - } - - public byte[] GetHashAndReset() - { - return hasher.GetHashAndReset(); - } - - public string GetHashStringAndReset() - { - return Convert.ToBase64String(GetHashAndReset()); - } - - public override void Flush() - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs deleted file mode 100644 index 207a626f8..000000000 --- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public interface IAssetStore - { - string GeneratePublicUrl(string fileName); - - Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default); - - Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default); - - Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default); - - Task DeleteAsync(string fileName); - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs deleted file mode 100644 index d4e46c533..000000000 --- a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public interface IAssetThumbnailGenerator - { - Task GetImageInfoAsync(Stream source); - - Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null); - } -} diff --git a/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/src/Squidex.Infrastructure/Assets/ImageInfo.cs deleted file mode 100644 index 2b3114cf3..000000000 --- a/src/Squidex.Infrastructure/Assets/ImageInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Assets -{ - public sealed class ImageInfo - { - public int PixelWidth { get; } - - public int PixelHeight { get; } - - public ImageInfo(int pixelWidth, int pixelHeight) - { - Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth)); - Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight)); - - PixelWidth = pixelWidth; - PixelHeight = pixelHeight; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs deleted file mode 100644 index 929acb63d..000000000 --- a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Transforms; -using SixLabors.Primitives; - -namespace Squidex.Infrastructure.Assets.ImageSharp -{ - public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator - { - public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null) - { - return Task.Run(() => - { - if (!width.HasValue && !height.HasValue && !quality.HasValue) - { - source.CopyTo(destination); - - return; - } - - using (var sourceImage = Image.Load(source, out var format)) - { - var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); - - if (quality.HasValue) - { - encoder = new JpegEncoder { Quality = quality.Value }; - } - - if (encoder == null) - { - throw new NotSupportedException(); - } - - if (width.HasValue || height.HasValue) - { - var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase); - - if (!Enum.TryParse(mode, true, out var resizeMode)) - { - resizeMode = ResizeMode.Max; - } - - if (isCropUpsize) - { - resizeMode = ResizeMode.Crop; - } - - var resizeWidth = width ?? 0; - var resizeHeight = height ?? 0; - - if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize) - { - resizeMode = ResizeMode.BoxPad; - } - - var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode }; - - sourceImage.Mutate(x => x.Resize(options)); - } - - sourceImage.Save(destination, encoder); - } - }); - } - - public Task GetImageInfoAsync(Stream source) - { - return Task.Run(() => - { - try - { - var image = Image.Load(source); - - return new ImageInfo(image.Width, image.Height); - } - catch - { - return null; - } - }); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs deleted file mode 100644 index 20d9fc363..000000000 --- a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public class MemoryAssetStore : IAssetStore - { - private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); - private readonly AsyncLock readerLock = new AsyncLock(); - private readonly AsyncLock writerLock = new AsyncLock(); - - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); - Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); - - if (!streams.TryGetValue(sourceFileName, out var sourceStream)) - { - throw new AssetNotFoundException(sourceFileName); - } - - using (await readerLock.LockAsync()) - { - await UploadAsync(targetFileName, sourceStream, false, ct); - } - } - - public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - if (!streams.TryGetValue(fileName, out var sourceStream)) - { - throw new AssetNotFoundException(fileName); - } - - using (await readerLock.LockAsync()) - { - try - { - await sourceStream.CopyToAsync(stream, 81920, ct); - } - finally - { - sourceStream.Position = 0; - } - } - } - - public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - Guard.NotNull(stream, nameof(stream)); - - var memoryStream = new MemoryStream(); - - async Task CopyAsync() - { - using (await writerLock.LockAsync()) - { - try - { - await stream.CopyToAsync(memoryStream, 81920, ct); - } - finally - { - memoryStream.Position = 0; - } - } - } - - if (overwrite) - { - await CopyAsync(); - - streams[fileName] = memoryStream; - } - else if (streams.TryAdd(fileName, memoryStream)) - { - await CopyAsync(); - } - else - { - throw new AssetAlreadyExistsException(fileName); - } - } - - public virtual Task DeleteAsync(string fileName) - { - Guard.NotNullOrEmpty(fileName, nameof(fileName)); - - streams.TryRemove(fileName, out _); - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs deleted file mode 100644 index 85ccd58c9..000000000 --- a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Assets -{ - public sealed class NoopAssetStore : IAssetStore - { - public string GeneratePublicUrl(string fileName) - { - return null; - } - - public Task CopyAsync(string sourceFileName, string fileName, CancellationToken ct = default) - { - throw new NotSupportedException(); - } - - public Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) - { - throw new NotSupportedException(); - } - - public Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) - { - throw new NotSupportedException(); - } - - public Task DeleteAsync(string fileName) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs b/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs deleted file mode 100644 index e6222aa37..000000000 --- a/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Threading; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Caching -{ - public sealed class AsyncLocalCache : ILocalCache - { - private static readonly AsyncLocal> LocalCache = new AsyncLocal>(); - private static readonly AsyncLocalCleaner> Cleaner; - - static AsyncLocalCache() - { - Cleaner = new AsyncLocalCleaner>(LocalCache); - } - - public IDisposable StartContext() - { - LocalCache.Value = new ConcurrentDictionary(); - - return Cleaner; - } - - public void Add(object key, object value) - { - var cacheKey = GetCacheKey(key); - - var cache = LocalCache.Value; - - if (cache != null) - { - cache[cacheKey] = value; - } - } - - public void Remove(object key) - { - var cacheKey = GetCacheKey(key); - - var cache = LocalCache.Value; - - if (cache != null) - { - cache.TryRemove(cacheKey, out _); - } - } - - public bool TryGetValue(object key, out object value) - { - var cacheKey = GetCacheKey(key); - - var cache = LocalCache.Value; - - if (cache != null) - { - return cache.TryGetValue(cacheKey, out value); - } - - value = null; - - return false; - } - - private static string GetCacheKey(object key) - { - return $"CACHE_{key}"; - } - } -} diff --git a/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs b/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs deleted file mode 100644 index 9ff773e51..000000000 --- a/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Caching.Memory; - -namespace Squidex.Infrastructure.Caching -{ - public abstract class CachingProviderBase - { - private readonly IMemoryCache cache; - - protected IMemoryCache Cache - { - get { return cache; } - } - - protected CachingProviderBase(IMemoryCache cache) - { - Guard.NotNull(cache, nameof(cache)); - - this.cache = cache; - } - } -} diff --git a/src/Squidex.Infrastructure/Caching/ILocalCache.cs b/src/Squidex.Infrastructure/Caching/ILocalCache.cs deleted file mode 100644 index 5eec26296..000000000 --- a/src/Squidex.Infrastructure/Caching/ILocalCache.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Caching -{ - public interface ILocalCache - { - IDisposable StartContext(); - - void Add(object key, object value); - - void Remove(object key); - - bool TryGetValue(object key, out object value); - } -} diff --git a/src/Squidex.Infrastructure/Caching/LRUCache.cs b/src/Squidex.Infrastructure/Caching/LRUCache.cs deleted file mode 100644 index 98f9c10f3..000000000 --- a/src/Squidex.Infrastructure/Caching/LRUCache.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Caching -{ - public sealed class LRUCache - { - private readonly Dictionary>> cacheMap = new Dictionary>>(); - private readonly LinkedList> cacheHistory = new LinkedList>(); - private readonly int capacity; - private readonly Action itemEvicted; - - public LRUCache(int capacity, Action itemEvicted = null) - { - Guard.GreaterThan(capacity, 0, nameof(capacity)); - - this.capacity = capacity; - - this.itemEvicted = itemEvicted ?? ((key, value) => { }); - } - - public bool Set(TKey key, TValue value) - { - if (cacheMap.TryGetValue(key, out var node)) - { - node.Value.Value = value; - - cacheHistory.Remove(node); - cacheHistory.AddLast(node); - - cacheMap[key] = node; - - return true; - } - - if (cacheMap.Count >= capacity) - { - RemoveFirst(); - } - - var cacheItem = new LRUCacheItem { Key = key, Value = value }; - - node = new LinkedListNode>(cacheItem); - - cacheMap.Add(key, node); - cacheHistory.AddLast(node); - - return false; - } - - public bool Remove(TKey key) - { - if (cacheMap.TryGetValue(key, out var node)) - { - cacheMap.Remove(key); - cacheHistory.Remove(node); - - return true; - } - - return false; - } - - public bool TryGetValue(TKey key, out object value) - { - value = null; - - if (cacheMap.TryGetValue(key, out var node)) - { - value = node.Value.Value; - - cacheHistory.Remove(node); - cacheHistory.AddLast(node); - - return true; - } - - return false; - } - - public bool Contains(TKey key) - { - return cacheMap.ContainsKey(key); - } - - private void RemoveFirst() - { - var node = cacheHistory.First; - - itemEvicted(node.Value.Key, node.Value.Value); - - cacheMap.Remove(node.Value.Key); - cacheHistory.RemoveFirst(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs b/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs deleted file mode 100644 index ff9cb3eef..000000000 --- a/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Infrastructure.Caching -{ - internal class LRUCacheItem - { - public TKey Key; - - public TValue Value; - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs deleted file mode 100644 index f5b12cc82..000000000 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ /dev/null @@ -1,244 +0,0 @@ -// ========================================================================== -// 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; - -namespace Squidex.Infrastructure -{ - public static class CollectionExtensions - { - public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class - { - return ResultList.Create(input.Total, SortList(input, idProvider, ids)); - } - - public static IEnumerable SortList(this IEnumerable input, Func idProvider, IReadOnlyList ids) where T : class - { - return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).Where(x => x != null); - } - - public static void AddRange(this ICollection target, IEnumerable source) - { - foreach (var value in source) - { - target.Add(value); - } - } - - public static IEnumerable Shuffle(this IEnumerable enumerable) - { - var random = new Random(); - - return enumerable.OrderBy(x => random.Next()).ToList(); - } - - public static HashSet ToHashSet(this IEnumerable enumerable) - { - return new HashSet(enumerable); - } - - public static HashSet ToHashSet(this IEnumerable enumerable, IEqualityComparer comparer) - { - return new HashSet(enumerable, comparer); - } - - public static IEnumerable OrEmpty(this IEnumerable source) - { - return source ?? Enumerable.Empty(); - } - - public static IEnumerable Concat(this IEnumerable source, T value) - { - return source.Concat(Enumerable.Repeat(value, 1)); - } - - public static TResult[] Map(this T[] value, Func convert) - { - var result = new TResult[value.Length]; - - for (var i = 0; i < value.Length; i++) - { - result[i] = convert(value[i]); - } - - return result; - } - - public static int SequentialHashCode(this IEnumerable collection) - { - return collection.SequentialHashCode(EqualityComparer.Default); - } - - public static int SequentialHashCode(this IEnumerable collection, IEqualityComparer comparer) - { - var hashCode = 17; - - foreach (var item in collection) - { - if (!Equals(item, null)) - { - hashCode = (hashCode * 23) + comparer.GetHashCode(item); - } - } - - return hashCode; - } - - public static int OrderedHashCode(this IEnumerable collection) - { - return collection.OrderedHashCode(EqualityComparer.Default); - } - - public static int OrderedHashCode(this IEnumerable collection, IEqualityComparer comparer) - { - Guard.NotNull(comparer, nameof(comparer)); - - var hashCodes = collection.Where(x => !Equals(x, null)).Select(x => x.GetHashCode()).OrderBy(x => x).ToArray(); - - var hashCode = 17; - - foreach (var code in hashCodes) - { - hashCode = (hashCode * 23) + code; - } - - return hashCode; - } - - public static int DictionaryHashCode(this IDictionary dictionary) - { - return DictionaryHashCode(dictionary, EqualityComparer.Default, EqualityComparer.Default); - } - - public static int DictionaryHashCode(this IDictionary dictionary, IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - var hashCode = 17; - - foreach (var kvp in dictionary.OrderBy(x => x.Key)) - { - hashCode = (hashCode * 23) + keyComparer.GetHashCode(kvp.Key); - - if (!Equals(kvp.Value, null)) - { - hashCode = (hashCode * 23) + valueComparer.GetHashCode(kvp.Value); - } - } - - return hashCode; - } - - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other) - { - return EqualsDictionary(dictionary, other, EqualityComparer.Default, EqualityComparer.Default); - } - - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - var comparer = new KeyValuePairComparer(keyComparer, valueComparer); - - return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any(); - } - - public static TValue GetOrDefault(this IReadOnlyDictionary dictionary, TKey key) - { - return dictionary.GetOrCreate(key, _ => default); - } - - public static TValue GetOrAddDefault(this IDictionary dictionary, TKey key) - { - return dictionary.GetOrAdd(key, _ => default); - } - - public static TValue GetOrNew(this IReadOnlyDictionary dictionary, TKey key) where TValue : class, new() - { - return dictionary.GetOrCreate(key, _ => new TValue()); - } - - public static TValue GetOrAddNew(this IDictionary dictionary, TKey key) where TValue : class, new() - { - return dictionary.GetOrAdd(key, _ => new TValue()); - } - - public static TValue GetOrCreate(this IReadOnlyDictionary dictionary, TKey key, Func creator) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = creator(key); - } - - return result; - } - - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TValue fallback) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = fallback; - - dictionary.Add(key, result); - } - - return result; - } - - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func creator) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = creator(key); - - dictionary.Add(key, result); - } - - return result; - } - - public static TValue GetOrAdd(this IDictionary dictionary, TKey key, TContext context, Func creator) - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = creator(key, context); - - dictionary.Add(key, result); - } - - return result; - } - - public static void Foreach(this IEnumerable collection, Action action) - { - foreach (var item in collection) - { - action(item); - } - } - - public sealed class KeyValuePairComparer : IEqualityComparer> - { - private readonly IEqualityComparer keyComparer; - private readonly IEqualityComparer valueComparer; - - public KeyValuePairComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - this.keyComparer = keyComparer; - this.valueComparer = valueComparer; - } - - public bool Equals(KeyValuePair x, KeyValuePair y) - { - return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value); - } - - public int GetHashCode(KeyValuePair obj) - { - return keyComparer.GetHashCode(obj.Key) ^ valueComparer.GetHashCode(obj.Value); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs b/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs deleted file mode 100644 index 1a6f1afd2..000000000 --- a/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Collections -{ - public static class ArrayDictionary - { - public static ArrayDictionary ToArrayDictionary(this IEnumerable source, Func keyExtractor) - { - return new ArrayDictionary(source.Select(x => new KeyValuePair(keyExtractor(x), x)).ToArray()); - } - } -} diff --git a/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs deleted file mode 100644 index 90d547bfa..000000000 --- a/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs +++ /dev/null @@ -1,164 +0,0 @@ -// ========================================================================== -// 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.Linq; - -namespace Squidex.Infrastructure.Collections -{ - public class ArrayDictionary : IReadOnlyDictionary - { - private readonly IEqualityComparer keyComparer; - private readonly KeyValuePair[] items; - - public TValue this[TKey key] - { - get - { - if (!TryGetValue(key, out var value)) - { - throw new KeyNotFoundException(); - } - - return value; - } - } - - public IEnumerable Keys - { - get { return items.Select(x => x.Key); } - } - - public IEnumerable Values - { - get { return items.Select(x => x.Value); } - } - - public int Count - { - get { return items.Length; } - } - - public ArrayDictionary() - : this(EqualityComparer.Default, Array.Empty>()) - { - } - - public ArrayDictionary(KeyValuePair[] items) - : this(EqualityComparer.Default, items) - { - } - - public ArrayDictionary(IEqualityComparer keyComparer, KeyValuePair[] items) - { - Guard.NotNull(items, nameof(items)); - Guard.NotNull(keyComparer, nameof(keyComparer)); - - this.items = items; - - this.keyComparer = keyComparer; - } - - public KeyValuePair[] With(TKey key, TValue value) - { - var result = new List>(Math.Max(items.Length, 1)); - - var wasReplaced = false; - - for (var i = 0; i < items.Length; i++) - { - var item = items[i]; - - if (wasReplaced || !keyComparer.Equals(item.Key, key)) - { - result.Add(item); - } - else - { - result.Add(new KeyValuePair(key, value)); - wasReplaced = true; - } - } - - if (!wasReplaced) - { - result.Add(new KeyValuePair(key, value)); - } - - return result.ToArray(); - } - - public KeyValuePair[] Without(TKey key) - { - var result = new List>(Math.Max(items.Length, 1)); - - var wasRemoved = false; - - for (var i = 0; i < items.Length; i++) - { - var item = items[i]; - - if (wasRemoved || !keyComparer.Equals(item.Key, key)) - { - result.Add(item); - } - else - { - wasRemoved = true; - } - } - - return result.ToArray(); - } - - public bool ContainsKey(TKey key) - { - for (var i = 0; i < items.Length; i++) - { - if (keyComparer.Equals(items[i].Key, key)) - { - return true; - } - } - - return false; - } - - public bool TryGetValue(TKey key, out TValue value) - { - value = default; - - for (var i = 0; i < items.Length; i++) - { - if (keyComparer.Equals(items[i].Key, key)) - { - value = items[i].Value; - return true; - } - } - - return false; - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - return GetEnumerable(items).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return items.GetEnumerator(); - } - - private static IEnumerable GetEnumerable(IEnumerable array) - { - return array; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/CommandContext.cs b/src/Squidex.Infrastructure/Commands/CommandContext.cs deleted file mode 100644 index 0b50bb495..000000000 --- a/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class CommandContext - { - private Tuple result; - - public Guid ContextId { get; } = Guid.NewGuid(); - - public ICommand Command { get; } - - public ICommandBus CommandBus { get; } - - public object PlainResult - { - get { return result?.Item1; } - } - - public bool IsCompleted - { - get { return result != null; } - } - - public CommandContext(ICommand command, ICommandBus commandBus) - { - Guard.NotNull(command, nameof(command)); - Guard.NotNull(commandBus, nameof(commandBus)); - - Command = command; - CommandBus = commandBus; - } - - public CommandContext Complete(object resultValue = null) - { - result = Tuple.Create(resultValue); - - return this; - } - - public T Result() - { - return (T)result?.Item1; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs b/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs deleted file mode 100644 index 556f7d1f8..000000000 --- a/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class CustomCommandMiddlewareRunner : ICommandMiddleware - { - private readonly IEnumerable extensions; - - public CustomCommandMiddlewareRunner(IEnumerable extensions) - { - Guard.NotNull(extensions, nameof(extensions)); - - this.extensions = extensions.Reverse().ToList(); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - foreach (var handler in extensions) - { - next = Join(handler, context, next); - } - - await next(); - } - - private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) - { - return () => handler.HandleAsync(context, next); - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs deleted file mode 100644 index 21d4a7436..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() - { - private readonly IStore store; - private T snapshot = new T { Version = EtagVersion.Empty }; - private IPersistence persistence; - - public override T Snapshot - { - get { return snapshot; } - } - - protected DomainObjectGrain(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - protected sealed override void ApplyEvent(Envelope @event) - { - var newVersion = Version + 1; - - snapshot = OnEvent(@event); - snapshot.Version = newVersion; - } - - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - snapshot = previousSnapshot; - } - - protected sealed override Task ReadAsync(Type type, Guid id) - { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), ApplyEvent); - - return persistence.ReadAsync(); - } - - private void ApplySnapshot(T state) - { - snapshot = state; - } - - protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) - { - if (events.Length > 0) - { - await persistence.WriteEventsAsync(events); - await persistence.WriteSnapshotAsync(Snapshot); - } - } - - protected T OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs deleted file mode 100644 index daf4c21c5..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs +++ /dev/null @@ -1,225 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() - { - private readonly List> uncomittedEvents = new List>(); - private readonly ISemanticLog log; - private Guid id; - - private enum Mode - { - Create, - Update, - Upsert - } - - public Guid Id - { - get { return id; } - } - - public long Version - { - get { return Snapshot.Version; } - } - - public abstract T Snapshot { get; } - - protected DomainObjectGrainBase(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - protected override async Task OnActivateAsync(Guid key) - { - var logContext = (key: key.ToString(), name: GetType().Name); - - using (log.MeasureInformation(logContext, (ctx, w) => w - .WriteProperty("action", "ActivateDomainObject") - .WriteProperty("domainObjectType", ctx.name) - .WriteProperty("domainObjectKey", ctx.key))) - { - id = key; - - await ReadAsync(GetType(), id); - } - } - - public void RaiseEvent(IEvent @event) - { - RaiseEvent(Envelope.Create(@event)); - } - - public virtual void RaiseEvent(Envelope @event) - { - Guard.NotNull(@event, nameof(@event)); - - @event.SetAggregateId(id); - - ApplyEvent(@event); - - uncomittedEvents.Add(@event); - } - - public IReadOnlyList> GetUncomittedEvents() - { - return uncomittedEvents; - } - - public void ClearUncommittedEvents() - { - uncomittedEvents.Clear(); - } - - protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Create); - } - - protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync(), Mode.Create); - } - - protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler.ToDefault(), Mode.Create); - } - - protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), Mode.Create); - } - - protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Update); - } - - protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync(), Mode.Update); - } - - protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault(), Mode.Update); - } - - protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), Mode.Update); - } - - protected Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Upsert); - } - - protected Task UpsertReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync(), Mode.Upsert); - } - - protected Task UpsertAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault(), Mode.Upsert); - } - - protected Task Upsert(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), Mode.Upsert); - } - - private async Task InvokeAsync(TCommand command, Func> handler, Mode mode) where TCommand : class, IAggregateCommand - { - Guard.NotNull(command, nameof(command)); - - if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) - { - throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); - } - - if (mode == Mode.Update && Version < 0) - { - TryDeactivateOnIdle(); - - throw new DomainObjectNotFoundException(id.ToString(), GetType()); - } - - if (mode == Mode.Create && Version >= 0) - { - throw new DomainException("Object has already been created."); - } - - var previousSnapshot = Snapshot; - var previousVersion = Version; - try - { - var result = await handler(command); - - var events = uncomittedEvents.ToArray(); - - await WriteAsync(events, previousVersion); - - if (result == null) - { - if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0)) - { - result = new EntitySavedResult(Version); - } - else - { - result = EntityCreatedResult.Create(id, Version); - } - } - - return result; - } - catch - { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - - throw; - } - finally - { - ClearUncommittedEvents(); - } - } - - protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); - - protected abstract void ApplyEvent(Envelope @event); - - protected abstract Task ReadAsync(Type type, Guid id); - - protected abstract Task WriteAsync(Envelope[] events, long previousVersion); - - public async Task> ExecuteAsync(J command) - { - var result = await ExecuteAsync(command.Value); - - return result; - } - - protected abstract Task ExecuteAsync(IAggregateCommand command); - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs deleted file mode 100644 index 68434279d..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public static class DomainObjectGrainFormatter - { - public static string Format(IGrainCallContext context) - { - if (context.InterfaceMethod == null) - { - return "Unknown"; - } - - if (string.Equals(context.InterfaceMethod.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && - context.Arguments?.Length == 1 && - context.Arguments[0] != null) - { - var argumentFullName = context.Arguments[0].ToString(); - var argumentParts = argumentFullName.Split('.'); - var argumentName = argumentParts[argumentParts.Length - 1]; - - return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; - } - - return context.InterfaceMethod.Name; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs deleted file mode 100644 index ba2d8b56c..000000000 --- a/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using NodaTime; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware - { - private readonly IClock clock; - - public EnrichWithTimestampCommandMiddleware(IClock clock) - { - Guard.NotNull(clock, nameof(clock)); - - this.clock = clock; - } - - public Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is ITimestampCommand timestampCommand) - { - timestampCommand.Timestamp = clock.GetCurrentInstant(); - } - - return next(); - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs deleted file mode 100644 index 9dfb70576..000000000 --- a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain - { - private readonly IGrainFactory grainFactory; - - public GrainCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public virtual async Task HandleAsync(CommandContext context, Func next) - { - await ExecuteCommandAsync(context); - - await next(); - } - - protected async Task ExecuteCommandAsync(CommandContext context) - { - if (context.Command is TCommand typedCommand) - { - var result = await ExecuteCommandAsync(typedCommand); - - context.Complete(result); - } - } - - private async Task ExecuteCommandAsync(TCommand typedCommand) - { - var grain = grainFactory.GetGrain(typedCommand.AggregateId); - - var result = await grain.ExecuteAsync(typedCommand); - - return result.Value; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs deleted file mode 100644 index f52ce2122..000000000 --- a/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public interface IDomainObjectGrain : IGrainWithGuidKey - { - Task> ExecuteAsync(J command); - } -} diff --git a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs deleted file mode 100644 index 7c72e8fba..000000000 --- a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class InMemoryCommandBus : ICommandBus - { - private readonly List middlewares; - - public InMemoryCommandBus(IEnumerable middlewares) - { - Guard.NotNull(middlewares, nameof(middlewares)); - - this.middlewares = middlewares.Reverse().ToList(); - } - - public async Task PublishAsync(ICommand command) - { - Guard.NotNull(command, nameof(command)); - - var context = new CommandContext(command, this); - - var next = new Func(() => TaskHelper.Done); - - foreach (var handler in middlewares) - { - next = Join(handler, context, next); - } - - await next(); - - return context; - } - - private static Func Join(ICommandMiddleware handler, CommandContext context, Func next) - { - return () => handler.HandleAsync(context, next); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs deleted file mode 100644 index 0e0d64504..000000000 --- a/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class LogCommandMiddleware : ICommandMiddleware - { - private readonly ISemanticLog log; - - public LogCommandMiddleware(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - var logContext = (id: context.ContextId.ToString(), command: context.Command.GetType().Name); - - try - { - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Started") - .WriteProperty("commandType", ctx.command)); - - using (log.MeasureInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Completed") - .WriteProperty("commandType", ctx.command))) - { - await next(); - } - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Succeeded") - .WriteProperty("commandType", ctx.command)); - } - catch (Exception ex) - { - log.LogError(ex, logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Failed") - .WriteProperty("commandType", ctx.command)); - - throw; - } - - if (!context.IsCompleted) - { - log.LogFatal(logContext, (ctx, w) => w - .WriteProperty("action", "HandleCommand.") - .WriteProperty("actionId", ctx.id) - .WriteProperty("status", "Unhandled") - .WriteProperty("commandType", ctx.command)); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs deleted file mode 100644 index 6ff25efc6..000000000 --- a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() - { - private readonly IStore store; - private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; - private IPersistence persistence; - - public override T Snapshot - { - get { return snapshots.Last(); } - } - - protected LogSnapshotDomainObjectGrain(IStore store, ISemanticLog log) - : base(log) - { - Guard.NotNull(log, nameof(log)); - - this.store = store; - } - - public T GetSnapshot(long version) - { - if (version == EtagVersion.Any || version == EtagVersion.Auto) - { - return Snapshot; - } - - if (version == EtagVersion.Empty) - { - return snapshots[0]; - } - - if (version >= 0 && version < snapshots.Count - 1) - { - return snapshots[(int)version + 1]; - } - - return default; - } - - protected sealed override void ApplyEvent(Envelope @event) - { - var snapshot = OnEvent(@event); - - snapshot.Version = Version + 1; - snapshots.Add(snapshot); - } - - protected sealed override Task ReadAsync(Type type, Guid id) - { - persistence = store.WithEventSourcing(type, id, ApplyEvent); - - return persistence.ReadAsync(); - } - - protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) - { - if (events.Length > 0) - { - var persistedSnapshots = store.GetSnapshotStore(); - - await persistence.WriteEventsAsync(events); - await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); - } - } - - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - while (snapshots.Count > previousVersion + 2) - { - snapshots.RemoveAt(snapshots.Count - 1); - } - } - - protected T OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs deleted file mode 100644 index c4c5ed3e9..000000000 --- a/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class ReadonlyCommandMiddleware : ICommandMiddleware - { - private readonly ReadonlyOptions options; - - public ReadonlyCommandMiddleware(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - this.options = options.Value; - } - - public Task HandleAsync(CommandContext context, Func next) - { - if (options.IsReadonly) - { - throw new DomainException("Application is in readonly mode at the moment."); - } - - return next(); - } - } -} diff --git a/src/Squidex.Infrastructure/DelegateDisposable.cs b/src/Squidex.Infrastructure/DelegateDisposable.cs deleted file mode 100644 index bbdbb0262..000000000 --- a/src/Squidex.Infrastructure/DelegateDisposable.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure -{ - public sealed class DelegateDisposable : IDisposable - { - private readonly Action action; - - public DelegateDisposable(Action action) - { - Guard.NotNull(action, nameof(action)); - - this.action = action; - } - - public void Dispose() - { - action(); - } - } -} diff --git a/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs b/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs deleted file mode 100644 index 3a7ebfb76..000000000 --- a/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Squidex.Infrastructure; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class DependencyInjectionExtensions - { - public delegate void Registrator(Type serviceType, Func implementationFactory); - - public sealed class InterfaceRegistrator - { - private readonly Registrator register; - private readonly Registrator registerOptional; - - public InterfaceRegistrator(Registrator register, Registrator registerOptional) - { - this.register = register; - this.registerOptional = registerOptional; - - var interfaces = typeof(T).GetInterfaces(); - - if (interfaces.Contains(typeof(IInitializable))) - { - register(typeof(IInitializable), c => c.GetRequiredService()); - } - - if (interfaces.Contains(typeof(IBackgroundProcess))) - { - register(typeof(IBackgroundProcess), c => c.GetRequiredService()); - } - } - - public InterfaceRegistrator AsSelf() - { - return this; - } - - public InterfaceRegistrator AsOptional() - { - if (typeof(TInterface) != typeof(T)) - { - registerOptional(typeof(TInterface), c => c.GetRequiredService()); - } - - return this; - } - - public InterfaceRegistrator As() - { - if (typeof(TInterface) != typeof(T)) - { - register(typeof(TInterface), c => c.GetRequiredService()); - } - - return this; - } - } - - public static InterfaceRegistrator AddTransientAs(this IServiceCollection services, Func factory) where T : class - { - services.AddTransient(typeof(T), factory); - - return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); - } - - public static InterfaceRegistrator AddTransientAs(this IServiceCollection services) where T : class - { - services.AddTransient(); - - return new InterfaceRegistrator((t, f) => services.AddTransient(t, f), services.TryAddTransient); - } - - public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, Func factory) where T : class - { - services.AddSingleton(typeof(T), factory); - - return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); - } - - public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services) where T : class - { - services.AddSingleton(); - - return new InterfaceRegistrator((t, f) => services.AddSingleton(t, f), services.TryAddSingleton); - } - } -} diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs deleted file mode 100644 index 7baf5921f..000000000 --- a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class GCHealthCheck : IHealthCheck - { - private readonly long threshold; - - public GCHealthCheck(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - threshold = 1024 * 1024 * options.Value.Threshold; - } - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var allocated = GC.GetTotalMemory(false); - - var data = new Dictionary - { - { "Allocated", allocated.ToReadableSize() }, - { "Gen0Collections", GC.CollectionCount(0) }, - { "Gen1Collections", GC.CollectionCount(1) }, - { "Gen2Collections", GC.CollectionCount(2) } - }; - - var status = allocated < threshold ? HealthStatus.Healthy : HealthStatus.Unhealthy; - - return Task.FromResult(new HealthCheckResult(status, $"Application must consum less than {threshold.ToReadableSize()} memory.", data: data)); - } - } -} diff --git a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs deleted file mode 100644 index 7d2495ae9..000000000 --- a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class OrleansHealthCheck : IHealthCheck - { - private readonly IManagementGrain managementGrain; - - public OrleansHealthCheck(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - managementGrain = grainFactory.GetGrain(0); - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var activationCount = await managementGrain.GetTotalActivationCount(); - - var status = activationCount > 0 ? HealthStatus.Healthy : HealthStatus.Unhealthy; - - return new HealthCheckResult(status, "Orleans must have at least one activation."); - } - } -} diff --git a/src/Squidex.Infrastructure/DomainException.cs b/src/Squidex.Infrastructure/DomainException.cs deleted file mode 100644 index 9adfb4053..000000000 --- a/src/Squidex.Infrastructure/DomainException.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure -{ - [Serializable] - public class DomainException : Exception - { - public DomainException(string message) - : base(message) - { - } - - public DomainException(string message, Exception inner) - : base(message, inner) - { - } - - protected DomainException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} diff --git a/src/Squidex.Infrastructure/DomainObjectException.cs b/src/Squidex.Infrastructure/DomainObjectException.cs deleted file mode 100644 index 24d311688..000000000 --- a/src/Squidex.Infrastructure/DomainObjectException.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure -{ - [Serializable] - public class DomainObjectException : Exception - { - private readonly string id; - private readonly string typeName; - - public string TypeName - { - get { return typeName; } - } - - public string Id - { - get { return id; } - } - - protected DomainObjectException(string message, string id, Type type, Exception inner = null) - : base(message, inner) - { - this.id = id; - - typeName = type?.Name; - } - - protected DomainObjectException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - id = info.GetString(nameof(id)); - - typeName = info.GetString(nameof(typeName)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(id), id); - info.AddValue(nameof(typeName), typeName); - - base.GetObjectData(info, context); - } - } -} diff --git a/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs deleted file mode 100644 index 1c73283f3..000000000 --- a/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using System.Net.Mail; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Email -{ - public sealed class SmtpEmailSender : IEmailSender - { - private readonly SmtpClient smtpClient; - private readonly string sender; - - public SmtpEmailSender(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - var config = options.Value; - - smtpClient = new SmtpClient(config.Server, config.Port) - { - Credentials = new NetworkCredential( - config.Username, - config.Password), - EnableSsl = config.EnableSsl - }; - - sender = config.Sender; - } - - public Task SendAsync(string recipient, string subject, string body) - { - return smtpClient.SendMailAsync(sender, recipient, subject, body); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs b/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs deleted file mode 100644 index 8e93b73f8..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/CompoundEventConsumer.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class CompoundEventConsumer : IEventConsumer - { - private readonly IEventConsumer[] inners; - - public string Name { get; } - - public string EventsFilter { get; } - - public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners) - : this(first?.Name, first, inners) - { - } - - public CompoundEventConsumer(IEventConsumer[] inners) - { - Guard.NotNull(inners, nameof(inners)); - Guard.NotEmpty(inners, nameof(inners)); - - this.inners = inners; - - Name = inners.First().Name; - - var innerFilters = - this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) - .Select(x => $"({x.EventsFilter})"); - - EventsFilter = string.Join("|", innerFilters); - } - - public CompoundEventConsumer(string name, IEventConsumer first, params IEventConsumer[] inners) - { - Guard.NotNull(first, nameof(first)); - Guard.NotNull(inners, nameof(inners)); - Guard.NotNullOrEmpty(name, nameof(name)); - - this.inners = new[] { first }.Union(inners).ToArray(); - - Name = name; - - var innerFilters = - this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) - .Select(x => $"({x.EventsFilter})"); - - EventsFilter = string.Join("|", innerFilters); - } - - public bool Handles(StoredEvent @event) - { - return inners.Any(x => x.Handles(@event)); - } - - public Task ClearAsync() - { - return Task.WhenAll(inners.Select(i => i.ClearAsync())); - } - - public async Task On(Envelope @event) - { - foreach (var inner in inners) - { - await inner.On(@event); - } - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs deleted file mode 100644 index 76ec225dd..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class DefaultEventDataFormatter : IEventDataFormatter - { - private readonly IJsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry; - - public DefaultEventDataFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - Guard.NotNull(serializer, nameof(serializer)); - - this.typeNameRegistry = typeNameRegistry; - - this.serializer = serializer; - } - - public Envelope Parse(EventData eventData, Func stringConverter = null) - { - var payloadType = typeNameRegistry.GetType(eventData.Type); - var payloadObj = serializer.Deserialize(eventData.Payload, payloadType, stringConverter); - - if (payloadObj is IMigrated migratedEvent) - { - payloadObj = migratedEvent.Migrate(); - - if (ReferenceEquals(migratedEvent, payloadObj)) - { - Debug.WriteLine("Migration should return new event."); - } - } - - var envelope = new Envelope(payloadObj, eventData.Headers); - - return envelope; - } - - public EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true) - { - var eventPayload = envelope.Payload; - - if (migrate && eventPayload is IMigrated migratedEvent) - { - eventPayload = migratedEvent.Migrate(); - - if (ReferenceEquals(migratedEvent, eventPayload)) - { - Debug.WriteLine("Migration should return new event."); - } - } - - var payloadType = typeNameRegistry.GetName(eventPayload.GetType()); - var payloadJson = serializer.Serialize(envelope.Payload); - - envelope.SetCommitId(commitId); - - return new EventData(payloadType, envelope.Headers, payloadJson); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs deleted file mode 100644 index ba1b59a34..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public class Envelope where T : class, IEvent - { - private readonly EnvelopeHeaders headers; - private readonly T payload; - - public EnvelopeHeaders Headers - { - get { return headers; } - } - - public T Payload - { - get { return payload; } - } - - public Envelope(T payload, EnvelopeHeaders headers = null) - { - Guard.NotNull(payload, nameof(payload)); - - this.payload = payload; - this.headers = headers ?? new EnvelopeHeaders(); - } - - public Envelope To() where TOther : class, IEvent - { - return new Envelope(payload as TOther, headers.Clone()); - } - - public static implicit operator Envelope(Envelope source) - { - return source == null ? source : new Envelope(source.payload, source.headers); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/src/Squidex.Infrastructure/EventSourcing/EventData.cs deleted file mode 100644 index 016043919..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/EventData.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class EventData - { - public EnvelopeHeaders Headers { get; } - - public string Payload { get; } - - public string Type { get; set; } - - public EventData(string type, EnvelopeHeaders headers, string payload) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(headers, nameof(headers)); - Guard.NotNull(payload, nameof(payload)); - - Headers = headers; - - Payload = payload; - - Type = type; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs deleted file mode 100644 index edfca3ddf..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ /dev/null @@ -1,305 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Orleans; -using Orleans.Concurrency; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerGrain : GrainOfString, IEventConsumerGrain - { - private readonly EventConsumerFactory eventConsumerFactory; - private readonly IGrainState state; - private readonly IEventDataFormatter eventDataFormatter; - private readonly IEventStore eventStore; - private readonly ISemanticLog log; - private TaskScheduler scheduler; - private IEventSubscription currentSubscription; - private IEventConsumer eventConsumer; - - public EventConsumerGrain( - EventConsumerFactory eventConsumerFactory, - IGrainState state, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - ISemanticLog log) - { - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); - Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory)); - Guard.NotNull(state, nameof(state)); - Guard.NotNull(log, nameof(log)); - - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.eventConsumerFactory = eventConsumerFactory; - this.state = state; - - this.log = log; - } - - protected override Task OnActivateAsync(string key) - { - scheduler = TaskScheduler.Current; - - eventConsumer = eventConsumerFactory(key); - - return TaskHelper.Done; - } - - public Task> GetStateAsync() - { - return Task.FromResult(CreateInfo()); - } - - private Immutable CreateInfo() - { - return state.Value.ToInfo(eventConsumer.Name).AsImmutable(); - } - - public Task OnEventAsync(Immutable subscription, Immutable storedEvent) - { - if (subscription.Value != currentSubscription) - { - return TaskHelper.Done; - } - - return DoAndUpdateStateAsync(async () => - { - if (eventConsumer.Handles(storedEvent.Value)) - { - var @event = ParseKnownEvent(storedEvent.Value); - - if (@event != null) - { - await DispatchConsumerAsync(@event); - } - } - - state.Value = state.Value.Handled(storedEvent.Value.EventPosition); - }); - } - - public Task OnErrorAsync(Immutable subscription, Immutable exception) - { - if (subscription.Value != currentSubscription) - { - return TaskHelper.Done; - } - - return DoAndUpdateStateAsync(() => - { - Unsubscribe(); - - state.Value = state.Value.Failed(exception.Value); - }); - } - - public Task ActivateAsync() - { - if (!state.Value.IsStopped) - { - Subscribe(state.Value.Position); - } - - return TaskHelper.Done; - } - - public async Task> StartAsync() - { - if (!state.Value.IsStopped) - { - return CreateInfo(); - } - - await DoAndUpdateStateAsync(() => - { - Subscribe(state.Value.Position); - - state.Value = state.Value.Started(); - }); - - return CreateInfo(); - } - - public async Task> StopAsync() - { - if (state.Value.IsStopped) - { - return CreateInfo(); - } - - await DoAndUpdateStateAsync(() => - { - Unsubscribe(); - - state.Value = state.Value.Stopped(); - }); - - return CreateInfo(); - } - - public async Task> ResetAsync() - { - await DoAndUpdateStateAsync(async () => - { - Unsubscribe(); - - await ClearAsync(); - - Subscribe(null); - - state.Value = state.Value.Reset(); - }); - - return CreateInfo(); - } - - private Task DoAndUpdateStateAsync(Action action, [CallerMemberName] string caller = null) - { - return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }, caller); - } - - private async Task DoAndUpdateStateAsync(Func action, [CallerMemberName] string caller = null) - { - try - { - await action(); - } - catch (Exception ex) - { - try - { - Unsubscribe(); - } - catch (Exception unsubscribeException) - { - ex = new AggregateException(ex, unsubscribeException); - } - - log.LogFatal(ex, w => w - .WriteProperty("action", caller) - .WriteProperty("status", "Failed") - .WriteProperty("eventConsumer", eventConsumer.Name)); - - state.Value = state.Value.Failed(ex); - } - - await state.WriteAsync(); - } - - private async Task ClearAsync() - { - var logContext = (actionId: Guid.NewGuid().ToString(), consumer: eventConsumer.Name); - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "EventConsumerReset") - .WriteProperty("actionId", ctx.actionId) - .WriteProperty("status", "Started") - .WriteProperty("eventConsumer", ctx.consumer)); - - using (log.MeasureTrace(logContext, (ctx, w) => w - .WriteProperty("action", "EventConsumerReset") - .WriteProperty("actionId", ctx.actionId) - .WriteProperty("status", "Completed") - .WriteProperty("eventConsumer", ctx.consumer))) - { - await eventConsumer.ClearAsync(); - } - } - - private async Task DispatchConsumerAsync(Envelope @event) - { - var eventId = @event.Headers.EventId().ToString(); - var eventType = @event.Payload.GetType().Name; - - var logContext = (eventId, eventType, consumer: eventConsumer.Name); - - log.LogInformation(logContext, (ctx, w) => w - .WriteProperty("action", "HandleEvent") - .WriteProperty("actionId", ctx.eventId) - .WriteProperty("status", "Started") - .WriteProperty("eventId", ctx.eventId) - .WriteProperty("eventType", ctx.eventType) - .WriteProperty("eventConsumer", ctx.consumer)); - - using (log.MeasureTrace(logContext, (ctx, w) => w - .WriteProperty("action", "HandleEvent") - .WriteProperty("actionId", ctx.eventId) - .WriteProperty("status", "Completed") - .WriteProperty("eventId", ctx.eventId) - .WriteProperty("eventType", ctx.eventType) - .WriteProperty("eventConsumer", ctx.consumer))) - { - await eventConsumer.On(@event); - } - } - - private void Unsubscribe() - { - if (currentSubscription != null) - { - currentSubscription.StopAsync().Forget(); - currentSubscription = null; - } - } - - private void Subscribe(string position) - { - if (currentSubscription == null) - { - currentSubscription?.StopAsync().Forget(); - currentSubscription = CreateSubscription(eventConsumer.EventsFilter, position); - } - else - { - currentSubscription.WakeUp(); - } - } - - private Envelope ParseKnownEvent(StoredEvent message) - { - try - { - var @event = eventDataFormatter.Parse(message.Data); - - @event.SetEventPosition(message.EventPosition); - @event.SetEventStreamNumber(message.EventStreamNumber); - - return @event; - } - catch (TypeNameNotFoundException) - { - log.LogDebug(w => w.WriteProperty("oldEventFound", message.Data.Type)); - - return null; - } - } - - protected virtual IEventConsumerGrain GetSelf() - { - return this.AsReference(); - } - - protected virtual IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position) - { - return new RetrySubscription(store, subscriber, streamFilter, position); - } - - private IEventSubscription CreateSubscription(string streamFilter, string position) - { - return CreateSubscription(eventStore, new WrapperSubscription(GetSelf(), scheduler), streamFilter, position); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs deleted file mode 100644 index 4952088c0..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// 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 System.Text.RegularExpressions; -using System.Threading.Tasks; -using Orleans; -using Orleans.Concurrency; -using Orleans.Core; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerManagerGrain : Grain, IEventConsumerManagerGrain, IRemindable - { - private readonly IEnumerable eventConsumers; - - public EventConsumerManagerGrain(IEnumerable eventConsumers) - : this(eventConsumers, null, null) - { - } - - protected EventConsumerManagerGrain( - IEnumerable eventConsumers, - IGrainIdentity identity, - IGrainRuntime runtime) - : base(identity, runtime) - { - Guard.NotNull(eventConsumers, nameof(eventConsumers)); - - this.eventConsumers = eventConsumers; - } - - public override Task OnActivateAsync() - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); - } - - public Task ActivateAsync(string streamName) - { - var tasks = - eventConsumers - .Where(c => streamName == null || Regex.IsMatch(streamName, c.EventsFilter)) - .Select(c => GrainFactory.GetGrain(c.Name)) - .Select(c => c.ActivateAsync()); - - return Task.WhenAll(tasks); - } - - public async Task>> GetConsumersAsync() - { - var tasks = - eventConsumers - .Select(c => GrainFactory.GetGrain(c.Name)) - .Select(c => c.GetStateAsync()); - - var consumerInfos = await Task.WhenAll(tasks); - - return new Immutable>(consumerInfos.Select(r => r.Value).ToList()); - } - - public Task StartAllAsync() - { - return Task.WhenAll( - eventConsumers - .Select(c => StartAsync(c.Name))); - } - - public Task StopAllAsync() - { - return Task.WhenAll( - eventConsumers - .Select(c => StopAsync(c.Name))); - } - - public Task> ResetAsync(string consumerName) - { - var eventConsumer = GrainFactory.GetGrain(consumerName); - - return eventConsumer.ResetAsync(); - } - - public Task> StartAsync(string consumerName) - { - var eventConsumer = GrainFactory.GetGrain(consumerName); - - return eventConsumer.StartAsync(); - } - - public Task> StopAsync(string consumerName) - { - var eventConsumer = GrainFactory.GetGrain(consumerName); - - return eventConsumer.StopAsync(); - } - - public Task ActivateAsync() - { - return ActivateAsync(null); - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return ActivateAsync(null); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs deleted file mode 100644 index 2b0ec771e..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public sealed class EventConsumerState - { - public bool IsStopped { get; set; } - - public string Error { get; set; } - - public string Position { get; set; } - - public EventConsumerState Reset() - { - return new EventConsumerState(); - } - - public EventConsumerState Handled(string position) - { - return new EventConsumerState { Position = position }; - } - - public EventConsumerState Failed(Exception ex) - { - return new EventConsumerState { Position = Position, IsStopped = true, Error = ex?.ToString() }; - } - - public EventConsumerState Stopped() - { - return new EventConsumerState { Position = Position, IsStopped = true }; - } - - public EventConsumerState Started() - { - return new EventConsumerState { Position = Position, IsStopped = false }; - } - - public EventConsumerInfo ToInfo(string name) - { - return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs deleted file mode 100644 index 6e3da7063..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public sealed class OrleansEventNotifier : IEventNotifier - { - private readonly Lazy eventConsumerManagerGrain; - - public OrleansEventNotifier(IGrainFactory factory) - { - Guard.NotNull(factory, nameof(factory)); - - eventConsumerManagerGrain = new Lazy(() => - { - return factory.GetGrain(SingleGrain.Id); - }); - } - - public void NotifyEventsStored(string streamName) - { - eventConsumerManagerGrain.Value.ActivateAsync(streamName); - } - - public IDisposable Subscribe(Action handler) - { - return null; - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs deleted file mode 100644 index db3c3fdff..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/IEventDataFormatter.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.EventSourcing -{ - public interface IEventDataFormatter - { - Envelope Parse(EventData eventData, Func stringConverter = null); - - EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true); - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs deleted file mode 100644 index 6ee608632..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// 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.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.EventSourcing -{ - public interface IEventStore - { - Task CreateIndexAsync(string property); - - Task> QueryAsync(string streamName, long streamPosition = 0); - - Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default); - - Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default); - - Task AppendAsync(Guid commitId, string streamName, ICollection events); - - Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); - - Task DeleteStreamAsync(string streamName); - - IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null); - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs deleted file mode 100644 index 59b8047cb..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.Timers; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class PollingSubscription : IEventSubscription - { - private readonly CompletionTimer timer; - - public PollingSubscription( - IEventStore eventStore, - IEventSubscriber eventSubscriber, - string streamFilter, - string position) - { - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); - - timer = new CompletionTimer(5000, async ct => - { - try - { - await eventStore.QueryAsync(async storedEvent => - { - await eventSubscriber.OnEventAsync(this, storedEvent); - - position = storedEvent.EventPosition; - }, streamFilter, position, ct); - } - catch (Exception ex) - { - if (!ex.Is()) - { - await eventSubscriber.OnErrorAsync(this, ex); - } - } - }); - } - - public void WakeUp() - { - timer.SkipCurrentDelay(); - } - - public Task StopAsync() - { - return timer.StopAsync(); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs deleted file mode 100644 index c995704ef..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -#pragma warning disable RECS0002 // Convert anonymous method to method group - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class RetrySubscription : IEventSubscription, IEventSubscriber - { - private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(10); - private readonly CancellationTokenSource timerCts = new CancellationTokenSource(); - private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); - private readonly IEventStore eventStore; - private readonly IEventSubscriber eventSubscriber; - private readonly string streamFilter; - private IEventSubscription currentSubscription; - private string position; - - public int ReconnectWaitMs { get; set; } = 5000; - - public RetrySubscription(IEventStore eventStore, IEventSubscriber eventSubscriber, string streamFilter, string position) - { - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); - Guard.NotNull(streamFilter, nameof(streamFilter)); - - this.position = position; - - this.eventStore = eventStore; - this.eventSubscriber = eventSubscriber; - - this.streamFilter = streamFilter; - - Subscribe(); - } - - private void Subscribe() - { - if (currentSubscription == null) - { - currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); - } - } - - private void Unsubscribe() - { - currentSubscription?.StopAsync().Forget(); - currentSubscription = null; - } - - public void WakeUp() - { - currentSubscription?.WakeUp(); - } - - private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - if (subscription == currentSubscription) - { - await eventSubscriber.OnEventAsync(this, storedEvent); - - position = storedEvent.EventPosition; - } - } - - private async Task HandleErrorAsync(IEventSubscription subscription, Exception exception) - { - if (subscription == currentSubscription) - { - Unsubscribe(); - - if (retryWindow.CanRetryAfterFailure()) - { - RetryAsync().Forget(); - } - else - { - await eventSubscriber.OnErrorAsync(this, exception); - } - } - } - - private async Task RetryAsync() - { - await Task.Delay(ReconnectWaitMs, timerCts.Token); - - await dispatcher.DispatchAsync(Subscribe); - } - - Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent)); - } - - Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) - { - return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); - } - - public async Task StopAsync() - { - await dispatcher.DispatchAsync(Unsubscribe); - await dispatcher.StopAndWaitAsync(); - - timerCts.Cancel(); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs deleted file mode 100644 index 97ee0f55c..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class StoredEvent - { - public string StreamName { get; } - - public string EventPosition { get; } - - public long EventStreamNumber { get; } - - public EventData Data { get; } - - public StoredEvent(string streamName, string eventPosition, long eventStreamNumber, EventData data) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNullOrEmpty(eventPosition, nameof(eventPosition)); - Guard.NotNull(data, nameof(data)); - - Data = data; - - EventPosition = eventPosition; - EventStreamNumber = eventStreamNumber; - - StreamName = streamName; - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs deleted file mode 100644 index b3bc063af..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.EventSourcing -{ - public static class StreamFilter - { - public static bool IsAll(string filter) - { - return string.IsNullOrWhiteSpace(filter) - || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) - || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) - || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); - } - } -} diff --git a/src/Squidex.Infrastructure/Guard.cs b/src/Squidex.Infrastructure/Guard.cs deleted file mode 100644 index c29b11827..000000000 --- a/src/Squidex.Infrastructure/Guard.cs +++ /dev/null @@ -1,219 +0,0 @@ -// ========================================================================== -// 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; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure -{ - public static class Guard - { - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidNumber(float target, string parameterName) - { - if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) - { - throw new ArgumentException("Value must be a valid number.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidNumber(double target, string parameterName) - { - if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) - { - throw new ArgumentException("Value must be a valid number.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidSlug(string target, string parameterName) - { - NotNullOrEmpty(target, parameterName); - - if (!target.IsSlug()) - { - throw new ArgumentException("Target is not a valid slug.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidPropertyName(string target, string parameterName) - { - NotNullOrEmpty(target, parameterName); - - if (!target.IsPropertyName()) - { - throw new ArgumentException("Target is not a valid property name.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void HasType(object target, string parameterName) - { - if (target != null && target.GetType() != typeof(T)) - { - throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void HasType(object target, Type expectedType, string parameterName) - { - if (target != null && expectedType != null && target.GetType() != expectedType) - { - throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Between(TValue target, TValue lower, TValue upper, string parameterName) where TValue : IComparable - { - if (!target.IsBetween(lower, upper)) - { - throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Enum(TEnum target, string parameterName) where TEnum : struct - { - if (!target.IsEnumValue()) - { - throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GreaterThan(TValue target, TValue lower, string parameterName) where TValue : IComparable - { - if (target.CompareTo(lower) <= 0) - { - throw new ArgumentException($"Value must be greater than {lower}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GreaterEquals(TValue target, TValue lower, string parameterName) where TValue : IComparable - { - if (target.CompareTo(lower) < 0) - { - throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LessThan(TValue target, TValue upper, string parameterName) where TValue : IComparable - { - if (target.CompareTo(upper) >= 0) - { - throw new ArgumentException($"Value must be less than {upper}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LessEquals(TValue target, TValue upper, string parameterName) where TValue : IComparable - { - if (target.CompareTo(upper) > 0) - { - throw new ArgumentException($"Value must be less or equal to {upper}", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(IReadOnlyCollection enumerable, string parameterName) - { - NotNull(enumerable, parameterName); - - if (enumerable.Count == 0) - { - throw new ArgumentException("Collection does not contain an item.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(Guid target, string parameterName) - { - if (target == Guid.Empty) - { - throw new ArgumentException("Value cannot be empty.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotNull(object target, string parameterName) - { - if (target == null) - { - throw new ArgumentNullException(parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotDefault(T target, string parameterName) - { - if (Equals(target, default(T))) - { - throw new ArgumentException("Value cannot be an the default value.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotNullOrEmpty(string target, string parameterName) - { - NotNull(target, parameterName); - - if (string.IsNullOrWhiteSpace(target)) - { - throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidFileName(string target, string parameterName) - { - NotNullOrEmpty(target, parameterName); - - if (target.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - throw new ArgumentException("Value contains an invalid character.", parameterName); - } - } - - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Valid(IValidatable target, string parameterName, Func message) - { - NotNull(target, parameterName); - - target.Validate(message); - } - } -} diff --git a/src/Squidex.Infrastructure/Http/DumpFormatter.cs b/src/Squidex.Infrastructure/Http/DumpFormatter.cs deleted file mode 100644 index 815236fac..000000000 --- a/src/Squidex.Infrastructure/Http/DumpFormatter.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -namespace Squidex.Infrastructure.Http -{ - public static class DumpFormatter - { - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string responseBody) - { - return BuildDump(request, response, null, responseBody, TimeSpan.Zero); - } - - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody) - { - return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); - } - - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody, TimeSpan elapsed, bool isTimeout = false) - { - var writer = new StringBuilder(); - - writer.AppendLine("Request:"); - writer.AppendRequest(request, requestBody); - - writer.AppendLine(); - writer.AppendLine(); - - writer.AppendLine("Response:"); - writer.AppendResponse(response, responseBody, elapsed, isTimeout); - - return writer.ToString(); - } - - private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string requestBody) - { - var method = request.Method.ToString().ToUpperInvariant(); - - writer.AppendLine($"{method}: {request.RequestUri} HTTP/{request.Version}"); - - writer.AppendHeaders(request.Headers); - writer.AppendHeaders(request.Content?.Headers); - - if (!string.IsNullOrWhiteSpace(requestBody)) - { - writer.AppendLine(); - writer.AppendLine(requestBody); - } - } - - private static void AppendResponse(this StringBuilder writer, HttpResponseMessage response, string responseBody, TimeSpan elapsed, bool isTimeout) - { - if (response != null) - { - var responseCode = (int)response.StatusCode; - var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); - - writer.AppendLine($"HTTP/{response.Version} {responseCode} {responseText}"); - - writer.AppendHeaders(response.Headers); - writer.AppendHeaders(response.Content?.Headers); - } - - if (!string.IsNullOrWhiteSpace(responseBody)) - { - writer.AppendLine(); - writer.AppendLine(responseBody); - } - - if (response != null && elapsed != TimeSpan.Zero) - { - writer.AppendLine(); - writer.AppendLine($"Elapsed: {elapsed}"); - } - - if (isTimeout) - { - writer.AppendLine($"Timeout after {elapsed}"); - } - } - - private static void AppendHeaders(this StringBuilder writer, HttpHeaders headers) - { - if (headers == null) - { - return; - } - - foreach (var header in headers) - { - writer.Append(header.Key); - writer.Append(": "); - writer.Append(string.Join("; ", header.Value)); - writer.AppendLine(); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Json/IJsonSerializer.cs b/src/Squidex.Infrastructure/Json/IJsonSerializer.cs deleted file mode 100644 index 704ebaa78..000000000 --- a/src/Squidex.Infrastructure/Json/IJsonSerializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; - -namespace Squidex.Infrastructure.Json -{ - public interface IJsonSerializer - { - string Serialize(T value, bool intented = false); - - void Serialize(T value, Stream stream); - - T Deserialize(string value, Type actualType = null, Func stringConverter = null); - - T Deserialize(Stream stream, Type actualType = null, Func stringConverter = null); - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs deleted file mode 100644 index a560e4abc..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver - { - private readonly JsonConverter[] converters; - private readonly object lockObject = new object(); - private Dictionary converterCache = new Dictionary(); - - public ConverterContractResolver(params JsonConverter[] converters) - { - NamingStrategy = new CamelCaseNamingStrategy(false, true); - - this.converters = converters; - - foreach (var converter in converters) - { - if (converter is ISupportedTypes supportedTypes) - { - foreach (var type in supportedTypes.SupportedTypes) - { - converterCache[type] = converter; - } - } - } - } - - protected override JsonArrayContract CreateArrayContract(Type objectType) - { - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) - { - var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments()); - - return base.CreateArrayContract(implementationType); - } - - return base.CreateArrayContract(objectType); - } - - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) - { - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) - { - var implementationType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments()); - - return base.CreateDictionaryContract(implementationType); - } - - return base.CreateDictionaryContract(objectType); - } - - protected override JsonConverter ResolveContractConverter(Type objectType) - { - var result = base.ResolveContractConverter(objectType); - - if (result != null) - { - return result; - } - - var cache = converterCache; - - if (cache == null || !cache.TryGetValue(objectType, out result)) - { - foreach (var converter in converters) - { - if (converter.CanConvert(objectType)) - { - result = converter; - } - } - - lock (lockObject) - { - cache = converterCache; - - var updatedCache = (cache != null) - ? new Dictionary(cache) - : new Dictionary(); - updatedCache[objectType] = result; - - converterCache = updatedCache; - } - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs deleted file mode 100644 index 9663accf9..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/InstantConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using NodaTime; -using NodaTime.Text; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class InstantConverter : JsonConverter - { - public IEnumerable SupportedTypes - { - get - { - yield return typeof(Instant); - yield return typeof(Instant?); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value != null) - { - writer.WriteValue(value.ToString()); - } - else - { - writer.WriteNull(); - } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.String) - { - return InstantPattern.General.Parse(reader.Value.ToString()).Value; - } - - if (reader.TokenType == JsonToken.Date) - { - return Instant.FromDateTimeUtc((DateTime)reader.Value); - } - - if (reader.TokenType == JsonToken.Null && objectType == typeof(Instant?)) - { - return null; - } - - throw new JsonException($"Not a valid date time, expected String or Date, but got {reader.TokenType}."); - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Instant) || objectType == typeof(Instant?); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs deleted file mode 100644 index 5bce0855f..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public abstract class JsonClassConverter : JsonConverter, ISupportedTypes where T : class - { - public virtual IEnumerable SupportedTypes - { - get { yield return typeof(T); } - } - - public sealed override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - return ReadValue(reader, objectType, serializer); - } - - protected abstract T ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer); - - public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - WriteValue(writer, (T)value, serializer); - } - - protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs deleted file mode 100644 index 49fdd5166..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs +++ /dev/null @@ -1,184 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Globalization; -using Newtonsoft.Json; -using Squidex.Infrastructure.Json.Objects; - -#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public class JsonValueConverter : JsonConverter, ISupportedTypes - { - private readonly HashSet supportedTypes = new HashSet - { - typeof(IJsonValue), - typeof(JsonArray), - typeof(JsonBoolean), - typeof(JsonNull), - typeof(JsonNumber), - typeof(JsonObject), - typeof(JsonString) - }; - - public virtual IEnumerable SupportedTypes - { - get { return supportedTypes; } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return ReadJson(reader); - } - - private static IJsonValue ReadJson(JsonReader reader) - { - switch (reader.TokenType) - { - case JsonToken.Comment: - reader.Read(); - break; - case JsonToken.StartObject: - { - var result = JsonValue.Object(); - - 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 = ReadJson(reader); - - result[propertyName] = value; - break; - case JsonToken.EndObject: - return result; - } - } - - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - case JsonToken.StartArray: - { - var result = JsonValue.Array(); - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.Comment: - break; - default: - var value = ReadJson(reader); - - result.Add(value); - break; - case JsonToken.EndArray: - return result; - } - } - - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - case JsonToken.Integer: - return JsonValue.Create((long)reader.Value); - case JsonToken.Float: - return JsonValue.Create((double)reader.Value); - case JsonToken.Boolean: - return JsonValue.Create((bool)reader.Value); - case JsonToken.Date: - return JsonValue.Create(((DateTime)reader.Value).ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture)); - case JsonToken.String: - return JsonValue.Create(reader.Value.ToString()); - case JsonToken.Null: - case JsonToken.Undefined: - return JsonValue.Null; - } - - throw new NotSupportedException(); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - WriteJson(writer, (IJsonValue)value); - } - - private static void WriteJson(JsonWriter writer, IJsonValue value) - { - switch (value) - { - case JsonNull _: - writer.WriteNull(); - break; - case JsonBoolean s: - writer.WriteValue(s.Value); - break; - case JsonString s: - writer.WriteValue(s.Value); - break; - case JsonNumber s: - - if (s.Value % 1 == 0) - { - writer.WriteValue((long)s.Value); - } - else - { - writer.WriteValue(s.Value); - } - - break; - case JsonArray array: - writer.WriteStartArray(); - - for (var i = 0; i < array.Count; i++) - { - WriteJson(writer, array[i]); - } - - writer.WriteEndArray(); - break; - - case JsonObject obj: - writer.WriteStartObject(); - - foreach (var kvp in obj) - { - writer.WritePropertyName(kvp.Key); - - WriteJson(writer, kvp.Value); - } - - writer.WriteEndObject(); - break; - } - } - - public override bool CanConvert(Type objectType) - { - return supportedTypes.Contains(objectType); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs deleted file mode 100644 index 3a79cd5d5..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class NewtonsoftJsonSerializer : IJsonSerializer - { - private readonly JsonSerializerSettings settings; - private readonly JsonSerializer serializer; - - private sealed class CustomReader : JsonTextReader - { - private readonly Func stringConverter; - - public override object Value - { - get - { - var value = base.Value; - - if (value is string s) - { - return stringConverter(s); - } - - return value; - } - } - - public CustomReader(TextReader reader, Func stringConverter) - : base(reader) - { - this.stringConverter = stringConverter; - } - } - - public NewtonsoftJsonSerializer(JsonSerializerSettings settings) - { - Guard.NotNull(settings, nameof(settings)); - - this.settings = settings; - - serializer = JsonSerializer.Create(settings); - } - - public string Serialize(T value, bool intented) - { - return JsonConvert.SerializeObject(value, intented ? Formatting.Indented : Formatting.None, settings); - } - - public void Serialize(T value, Stream stream) - { - using (var writer = new StreamWriter(stream)) - { - serializer.Serialize(writer, value); - - writer.Flush(); - } - } - - public T Deserialize(string value, Type actualType = null, Func stringConverter = null) - { - using (var textReader = new StringReader(value)) - { - actualType = actualType ?? typeof(T); - - using (var reader = GetReader(stringConverter, textReader)) - { - return (T)serializer.Deserialize(reader, actualType); - } - } - } - - public T Deserialize(Stream stream, Type actualType = null, Func stringConverter = null) - { - using (var textReader = new StreamReader(stream)) - { - actualType = actualType ?? typeof(T); - - using (var reader = GetReader(stringConverter, textReader)) - { - return (T)serializer.Deserialize(reader, actualType); - } - } - } - - private static JsonTextReader GetReader(Func stringConverter, TextReader textReader) - { - return stringConverter != null ? new CustomReader(textReader, stringConverter) : new JsonTextReader(textReader); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs deleted file mode 100644 index 1e3ac6a94..000000000 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json.Serialization; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class TypeNameSerializationBinder : DefaultSerializationBinder - { - private readonly TypeNameRegistry typeNameRegistry; - - public TypeNameSerializationBinder(TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.typeNameRegistry = typeNameRegistry; - } - - public override Type BindToType(string assemblyName, string typeName) - { - var type = typeNameRegistry.GetTypeOrNull(typeName); - - return type ?? base.BindToType(assemblyName, typeName); - } - - public override void BindToName(Type serializedType, out string assemblyName, out string typeName) - { - assemblyName = null; - - var name = typeNameRegistry.GetNameOrNull(serializedType); - - if (name != null) - { - typeName = name; - } - else - { - base.BindToName(serializedType, out assemblyName, out typeName); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs b/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs deleted file mode 100644 index 743cf4a2a..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Json.Objects -{ - public interface IJsonValue : IEquatable - { - JsonValueType Type { get; } - - string ToJsonString(); - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs deleted file mode 100644 index 50230c306..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace Squidex.Infrastructure.Json.Objects -{ - public sealed class JsonArray : Collection, IJsonValue, IEquatable - { - public JsonValueType Type - { - get { return JsonValueType.Array; } - } - - public JsonArray() - { - } - - internal JsonArray(params object[] values) - : base(ToList(values)) - { - } - - private static List ToList(IEnumerable values) - { - return values?.Select(JsonValue.Create).ToList() ?? new List(); - } - - protected override void InsertItem(int index, IJsonValue item) - { - base.InsertItem(index, item ?? JsonValue.Null); - } - - protected override void SetItem(int index, IJsonValue item) - { - base.SetItem(index, item ?? JsonValue.Null); - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonArray); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonArray); - } - - public bool Equals(JsonArray array) - { - if (array == null || array.Count != Count) - { - return false; - } - - for (var i = 0; i < Count; i++) - { - if (!this[i].Equals(array[i])) - { - return false; - } - } - - return true; - } - - public override int GetHashCode() - { - var hashCode = 17; - - for (var i = 0; i < Count; i++) - { - hashCode = (hashCode * 23) + this[i].GetHashCode(); - } - - return hashCode; - } - - public string ToJsonString() - { - return ToString(); - } - - public override string ToString() - { - return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs deleted file mode 100644 index 884462b3c..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Json.Objects -{ - public sealed class JsonNull : IJsonValue, IEquatable - { - public static readonly JsonNull Null = new JsonNull(); - - public JsonValueType Type - { - get { return JsonValueType.Null; } - } - - private JsonNull() - { - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonNull); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonNull); - } - - public bool Equals(JsonNull other) - { - return other != null; - } - - public override int GetHashCode() - { - return 0; - } - - public string ToJsonString() - { - return ToString(); - } - - public override string ToString() - { - return "null"; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs deleted file mode 100644 index 6eb6195d0..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ /dev/null @@ -1,135 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Json.Objects -{ - public class JsonObject : IReadOnlyDictionary, IJsonValue, IEquatable - { - private readonly Dictionary inner; - - public IJsonValue this[string key] - { - get - { - return inner[key]; - } - set - { - Guard.NotNull(key, nameof(key)); - - inner[key] = value ?? JsonValue.Null; - } - } - - public IEnumerable Keys - { - get { return inner.Keys; } - } - - public IEnumerable Values - { - get { return inner.Values; } - } - - public int Count - { - get { return inner.Count; } - } - - public JsonValueType Type - { - get { return JsonValueType.Array; } - } - - internal JsonObject() - { - inner = new Dictionary(); - } - - public JsonObject(JsonObject obj) - { - inner = new Dictionary(obj.inner); - } - - public JsonObject Add(string key, object value) - { - return Add(key, JsonValue.Create(value)); - } - - public JsonObject Add(string key, IJsonValue value) - { - inner[key] = value ?? JsonValue.Null; - - return this; - } - - public void Clear() - { - inner.Clear(); - } - - public bool Remove(string key) - { - return inner.Remove(key); - } - - public bool ContainsKey(string key) - { - return inner.ContainsKey(key); - } - - public bool TryGetValue(string key, out IJsonValue value) - { - return inner.TryGetValue(key, out value); - } - - public IEnumerator> GetEnumerator() - { - return inner.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return inner.GetEnumerator(); - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonObject); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonObject); - } - - public bool Equals(JsonObject other) - { - return other != null && inner.EqualsDictionary(other.inner); - } - - public override int GetHashCode() - { - return inner.DictionaryHashCode(); - } - - public string ToJsonString() - { - return ToString(); - } - - public override string ToString() - { - return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs b/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs deleted file mode 100644 index 5ea9c2bd7..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Json.Objects -{ - public abstract class JsonScalar : IJsonValue, IEquatable> - { - public abstract JsonValueType Type { get; } - - public T Value { get; } - - protected JsonScalar(T value) - { - Value = value; - } - - public override bool Equals(object obj) - { - return Equals(obj as JsonScalar); - } - - public bool Equals(IJsonValue other) - { - return Equals(other as JsonScalar); - } - - public bool Equals(JsonScalar other) - { - return other != null && other.Type == Type && Equals(other.Value, Value); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public override string ToString() - { - return Value.ToString(); - } - - public virtual string ToJsonString() - { - return ToString(); - } - } -} diff --git a/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs deleted file mode 100644 index 922039f5e..000000000 --- a/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using NodaTime; - -#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - -namespace Squidex.Infrastructure.Json.Objects -{ - public static class JsonValue - { - public static readonly IJsonValue Empty = new JsonString(string.Empty); - - public static readonly IJsonValue True = JsonBoolean.True; - public static readonly IJsonValue False = JsonBoolean.False; - - public static readonly IJsonValue Null = JsonNull.Null; - - public static readonly IJsonValue Zero = new JsonNumber(0); - - public static JsonArray Array() - { - return new JsonArray(); - } - - public static JsonArray Array(params object[] values) - { - return new JsonArray(values); - } - - public static JsonObject Object() - { - return new JsonObject(); - } - - public static IJsonValue Create(object value) - { - if (value == null) - { - return Null; - } - - if (value is IJsonValue v) - { - return v; - } - - switch (value) - { - case string s: - return Create(s); - case bool b: - return Create(b); - case float f: - return Create(f); - case double d: - return Create(d); - case int i: - return Create(i); - case long l: - return Create(l); - case Instant i: - return Create(i); - } - - throw new ArgumentException("Invalid json type"); - } - - public static IJsonValue Create(bool value) - { - return value ? True : False; - } - - public static IJsonValue Create(double value) - { - Guard.ValidNumber(value, nameof(value)); - - if (value == 0) - { - return Zero; - } - - return new JsonNumber(value); - } - - public static IJsonValue Create(Instant? value) - { - if (value == null) - { - return Null; - } - - return Create(value.Value.ToString()); - } - - public static IJsonValue Create(double? value) - { - if (value == null) - { - return Null; - } - - return Create(value.Value); - } - - public static IJsonValue Create(bool? value) - { - if (value == null) - { - return Null; - } - - return Create(value.Value); - } - - public static IJsonValue Create(string value) - { - if (value == null) - { - return Null; - } - - if (value.Length == 0) - { - return Empty; - } - - return new JsonString(value); - } - } -} diff --git a/src/Squidex.Infrastructure/Language.cs b/src/Squidex.Infrastructure/Language.cs deleted file mode 100644 index 0d096fc31..000000000 --- a/src/Squidex.Infrastructure/Language.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// 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.Text.RegularExpressions; - -namespace Squidex.Infrastructure -{ - public sealed partial class Language - { - private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); - private static readonly Dictionary AllLanguagesField = new Dictionary(StringComparer.OrdinalIgnoreCase); - - internal static Language AddLanguage(string iso2Code, string englishName) - { - return AllLanguagesField.GetOrAdd(iso2Code, englishName, (c, n) => new Language(c, n)); - } - - public static Language GetLanguage(string iso2Code) - { - Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - - try - { - return AllLanguagesField[iso2Code]; - } - catch (KeyNotFoundException) - { - throw new NotSupportedException($"Language {iso2Code} is not supported"); - } - } - - public static IReadOnlyCollection AllLanguages - { - get { return AllLanguagesField.Values; } - } - - public string EnglishName { get; } - - public string Iso2Code { get; } - - private Language(string iso2Code, string englishName) - { - Iso2Code = iso2Code; - - EnglishName = englishName; - } - - public static bool IsValidLanguage(string iso2Code) - { - Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - - return AllLanguagesField.ContainsKey(iso2Code); - } - - public static bool TryGetLanguage(string iso2Code, out Language language) - { - Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - - return AllLanguagesField.TryGetValue(iso2Code, out language); - } - - public static implicit operator string(Language language) - { - return language?.Iso2Code; - } - - public static implicit operator Language(string iso2Code) - { - return GetLanguage(iso2Code); - } - - public static Language ParseOrNull(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - return null; - } - - input = input.Trim(); - - if (input.Length != 2) - { - var match = CultureRegex.Match(input); - - if (!match.Success) - { - return null; - } - - input = match.Groups[1].Value; - } - - if (TryGetLanguage(input.ToLowerInvariant(), out var result)) - { - return result; - } - - return null; - } - - public override string ToString() - { - return EnglishName; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/LanguagesInitializer.cs b/src/Squidex.Infrastructure/LanguagesInitializer.cs deleted file mode 100644 index 214e7aa60..000000000 --- a/src/Squidex.Infrastructure/LanguagesInitializer.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure -{ - public sealed class LanguagesInitializer : IInitializable - { - private readonly LanguagesOptions options; - - public LanguagesInitializer(IOptions options) - { - Guard.NotNull(options, nameof(options)); - - this.options = options.Value; - } - - public Task InitializeAsync(CancellationToken ct = default) - { - foreach (var kvp in options) - { - if (!string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value)) - { - Language.AddLanguage(kvp.Key, kvp.Value); - } - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs b/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs deleted file mode 100644 index 524145b0f..000000000 --- a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Squidex.Infrastructure.Log.Adapter -{ - public class SemanticLogLoggerProvider : ILoggerProvider - { - private readonly IServiceProvider services; - private ISemanticLog log; - - public SemanticLogLoggerProvider(IServiceProvider services) - { - Guard.NotNull(services, nameof(services)); - - this.services = services; - } - - internal SemanticLogLoggerProvider(ISemanticLog log) - { - this.log = log; - } - - public static SemanticLogLoggerProvider ForTesting(ISemanticLog log) - { - return new SemanticLogLoggerProvider(log); - } - - public ILogger CreateLogger(string categoryName) - { - if (log == null && services != null) - { - log = services.GetService(typeof(ISemanticLog)) as ISemanticLog; - } - - if (log == null) - { - return NullLogger.Instance; - } - - return new SemanticLogLogger(log.CreateScope(writer => - { - writer.WriteProperty("category", categoryName); - })); - } - - public void Dispose() - { - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs deleted file mode 100644 index 3f78d1efd..000000000 --- a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Reflection; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ApplicationInfoLogAppender : ILogAppender - { - private readonly string applicationName; - private readonly string applicationVersion; - private readonly string applicationSessionId; - - public ApplicationInfoLogAppender(Type type, Guid applicationSession) - : this(type?.Assembly, applicationSession) - { - } - - public ApplicationInfoLogAppender(Assembly assembly, Guid applicationSession) - { - Guard.NotNull(assembly, nameof(assembly)); - - applicationName = assembly.GetName().Name; - applicationVersion = assembly.GetName().Version.ToString(); - applicationSessionId = applicationSession.ToString(); - } - - public void Append(IObjectWriter writer, SemanticLogLevel logLevel) - { - writer.WriteObject("app", w => w - .WriteProperty("name", applicationName) - .WriteProperty("version", applicationVersion) - .WriteProperty("sessionId", applicationSessionId)); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs b/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs deleted file mode 100644 index 9ab7abeb2..000000000 --- a/src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ConstantsLogWriter : ILogAppender - { - private readonly Action objectWriter; - - public ConstantsLogWriter(Action objectWriter) - { - Guard.NotNull(objectWriter, nameof(objectWriter)); - - this.objectWriter = objectWriter; - } - - public void Append(IObjectWriter writer, SemanticLogLevel logLevel) - { - objectWriter(writer); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/FileChannel.cs b/src/Squidex.Infrastructure/Log/FileChannel.cs deleted file mode 100644 index 26a8392e6..000000000 --- a/src/Squidex.Infrastructure/Log/FileChannel.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Log.Internal; - -namespace Squidex.Infrastructure.Log -{ - public sealed class FileChannel : DisposableObjectBase, ILogChannel - { - private readonly FileLogProcessor processor; - private readonly object lockObject = new object(); - private volatile bool isInitialized; - - public FileChannel(string path) - { - Guard.NotNullOrEmpty(path, nameof(path)); - - processor = new FileLogProcessor(path); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - processor.Dispose(); - } - } - - public void Log(SemanticLogLevel logLevel, string message) - { - if (!isInitialized) - { - lock (lockObject) - { - if (!isInitialized) - { - processor.Initialize(); - - isInitialized = true; - } - } - } - - processor.EnqueueMessage(new LogMessageEntry { Message = message }); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/IObjectWriter.cs b/src/Squidex.Infrastructure/Log/IObjectWriter.cs deleted file mode 100644 index 98f02aed3..000000000 --- a/src/Squidex.Infrastructure/Log/IObjectWriter.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using NodaTime; - -namespace Squidex.Infrastructure.Log -{ - public interface IObjectWriter - { - IObjectWriter WriteProperty(string property, string value); - - IObjectWriter WriteProperty(string property, double value); - - IObjectWriter WriteProperty(string property, long value); - - IObjectWriter WriteProperty(string property, bool value); - - IObjectWriter WriteProperty(string property, TimeSpan value); - - IObjectWriter WriteProperty(string property, Instant value); - - IObjectWriter WriteObject(string property, Action objectWriter); - - IObjectWriter WriteObject(string property, T context, Action objectWriter); - - IObjectWriter WriteArray(string property, Action arrayWriter); - - IObjectWriter WriteArray(string property, T context, Action arrayWriter); - } -} diff --git a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs deleted file mode 100644 index 2957ceb63..000000000 --- a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Squidex.Infrastructure.Log.Internal -{ - public sealed class ConsoleLogProcessor : DisposableObjectBase - { - private const int MaxQueuedMessages = 1024; - private readonly IConsole console; - private readonly BlockingCollection messageQueue = new BlockingCollection(MaxQueuedMessages); - private readonly Thread outputThread; - - public ConsoleLogProcessor() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - console = new WindowsLogConsole(true); - } - else - { - console = new AnsiLogConsole(false); - } - - outputThread = new Thread(ProcessLogQueue) - { - IsBackground = true, Name = "Logging" - }; - outputThread.Start(); - } - - public void EnqueueMessage(LogMessageEntry message) - { - if (!messageQueue.IsAddingCompleted) - { - try - { - messageQueue.Add(message); - return; - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to enqueue log message: {ex}."); - } - } - - WriteMessage(message); - } - - private void ProcessLogQueue() - { - try - { - foreach (var message in messageQueue.GetConsumingEnumerable()) - { - WriteMessage(message); - } - } - catch - { - try - { - messageQueue.CompleteAdding(); - } - catch - { - return; - } - } - } - - private void WriteMessage(LogMessageEntry entry) - { - console.WriteLine(entry.Color, entry.Message); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - messageQueue.CompleteAdding(); - - try - { - outputThread.Join(1500); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to shutdown log queue grateful: {ex}."); - } - finally - { - console.Reset(); - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs b/src/Squidex.Infrastructure/Log/JsonLogWriter.cs deleted file mode 100644 index 13b0c1e91..000000000 --- a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs +++ /dev/null @@ -1,225 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Text; -using System.Text.Json; -using NodaTime; - -namespace Squidex.Infrastructure.Log -{ - public sealed class JsonLogWriter : IObjectWriter, IArrayWriter - { - private readonly JsonWriterOptions formatting; - private readonly bool formatLine; - private readonly MemoryStream stream = new MemoryStream(); - private readonly StreamReader streamReader; - private Utf8JsonWriter jsonWriter; - - public long BufferSize - { - get { return stream.Length; } - } - - internal JsonLogWriter(JsonWriterOptions formatting, bool formatLine) - { - this.formatLine = formatLine; - this.formatting = formatting; - - streamReader = new StreamReader(stream, Encoding.UTF8); - - Start(); - } - - private void Start() - { - jsonWriter = new Utf8JsonWriter(stream, formatting); - jsonWriter.WriteStartObject(); - } - - internal void Reset() - { - stream.Position = 0; - stream.SetLength(0); - - Start(); - } - - IArrayWriter IArrayWriter.WriteValue(string value) - { - jsonWriter.WriteStringValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(double value) - { - jsonWriter.WriteNumberValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(long value) - { - jsonWriter.WriteNumberValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(bool value) - { - jsonWriter.WriteBooleanValue(value); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(Instant value) - { - jsonWriter.WriteStringValue(value.ToString()); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(TimeSpan value) - { - jsonWriter.WriteStringValue(value.ToString()); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, string value) - { - jsonWriter.WriteString(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, double value) - { - jsonWriter.WriteNumber(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, long value) - { - jsonWriter.WriteNumber(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, bool value) - { - jsonWriter.WriteBoolean(property, value); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, Instant value) - { - jsonWriter.WriteString(property, value.ToString()); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value) - { - jsonWriter.WriteString(property, value.ToString()); - - return this; - } - - IObjectWriter IObjectWriter.WriteObject(string property, Action objectWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(this); - - jsonWriter.WriteEndObject(); - - return this; - } - - IObjectWriter IObjectWriter.WriteObject(string property, T context, Action objectWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(context, this); - - jsonWriter.WriteEndObject(); - - return this; - } - - IObjectWriter IObjectWriter.WriteArray(string property, Action arrayWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartArray(); - - arrayWriter?.Invoke(this); - - jsonWriter.WriteEndArray(); - - return this; - } - - IObjectWriter IObjectWriter.WriteArray(string property, T context, Action arrayWriter) - { - jsonWriter.WritePropertyName(property); - jsonWriter.WriteStartArray(); - - arrayWriter?.Invoke(context, this); - - jsonWriter.WriteEndArray(); - - return this; - } - - IArrayWriter IArrayWriter.WriteObject(Action objectWriter) - { - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(this); - - jsonWriter.WriteEndObject(); - - return this; - } - - IArrayWriter IArrayWriter.WriteObject(T context, Action objectWriter) - { - jsonWriter.WriteStartObject(); - - objectWriter?.Invoke(context, this); - - jsonWriter.WriteEndObject(); - - return this; - } - - public override string ToString() - { - jsonWriter.WriteEndObject(); - jsonWriter.Flush(); - - stream.Position = 0; - streamReader.DiscardBufferedData(); - - var json = streamReader.ReadToEnd(); - - if (formatLine) - { - json += Environment.NewLine; - } - - return json; - } - } -} diff --git a/src/Squidex.Infrastructure/Log/LockingLogStore.cs b/src/Squidex.Infrastructure/Log/LockingLogStore.cs deleted file mode 100644 index a5b7e02dc..000000000 --- a/src/Squidex.Infrastructure/Log/LockingLogStore.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.Log -{ - public sealed class LockingLogStore : ILogStore - { - private static readonly byte[] LockedText = Encoding.UTF8.GetBytes("Another process is currenty running, try it again later."); - private static readonly TimeSpan LockWaitingTime = TimeSpan.FromMinutes(1); - private readonly ILogStore inner; - private readonly ILockGrain lockGrain; - - public LockingLogStore(ILogStore inner, IGrainFactory grainFactory) - { - Guard.NotNull(inner, nameof(inner)); - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.inner = inner; - - lockGrain = grainFactory.GetGrain(SingleGrain.Id); - } - - public Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream) - { - return ReadLogAsync(key, from, to, stream, LockWaitingTime); - } - - public async Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream, TimeSpan lockTimeout) - { - using (var cts = new CancellationTokenSource(lockTimeout)) - { - string releaseToken = null; - - while (!cts.IsCancellationRequested) - { - releaseToken = await lockGrain.AcquireLockAsync(key); - - if (releaseToken != null) - { - break; - } - - try - { - await Task.Delay(2000, cts.Token); - } - catch (OperationCanceledException) - { - break; - } - } - - if (releaseToken != null) - { - try - { - await inner.ReadLogAsync(key, from, to, stream); - } - finally - { - await lockGrain.ReleaseLockAsync(releaseToken); - } - } - else - { - await stream.WriteAsync(LockedText, 0, LockedText.Length); - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/Profiler.cs b/src/Squidex.Infrastructure/Log/Profiler.cs deleted file mode 100644 index e2a5eed33..000000000 --- a/src/Squidex.Infrastructure/Log/Profiler.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.CompilerServices; -using System.Threading; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Log -{ - public delegate void ProfilerStarted(ProfilerSpan span); - - public static class Profiler - { - private static readonly AsyncLocal LocalSession = new AsyncLocal(); - private static readonly AsyncLocalCleaner Cleaner; - - public static ProfilerSession Session - { - get { return LocalSession.Value; } - } - - public static event ProfilerStarted SpanStarted; - - static Profiler() - { - Cleaner = new AsyncLocalCleaner(LocalSession); - } - - public static IDisposable StartSession() - { - LocalSession.Value = new ProfilerSession(); - - return Cleaner; - } - - public static IDisposable TraceMethod(Type type, [CallerMemberName] string memberName = null) - { - return Trace($"{type.Name}/{memberName}"); - } - - public static IDisposable TraceMethod([CallerMemberName] string memberName = null) - { - return Trace($"{typeof(T).Name}/{memberName}"); - } - - public static IDisposable TraceMethod(string objectName, [CallerMemberName] string memberName = null) - { - return Trace($"{objectName}/{memberName}"); - } - - public static IDisposable Trace(string key) - { - Guard.NotNull(key, nameof(key)); - - var session = LocalSession.Value; - - if (session == null) - { - return NoopDisposable.Instance; - } - - var span = new ProfilerSpan(session, key); - - SpanStarted?.Invoke(span); - - return span; - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ProfilerSession.cs b/src/Squidex.Infrastructure/Log/ProfilerSession.cs deleted file mode 100644 index 6ecbd3d31..000000000 --- a/src/Squidex.Infrastructure/Log/ProfilerSession.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ProfilerSession - { - private struct ProfilerItem - { - public long Total; - public long Count; - } - - private readonly ConcurrentDictionary traces = new ConcurrentDictionary(); - - public void Measured(string name, long elapsed) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - traces.AddOrUpdate(name, x => - { - return new ProfilerItem { Total = elapsed, Count = 1 }; - }, - (x, result) => - { - result.Total += elapsed; - result.Count++; - - return result; - }); - } - - public void Write(IObjectWriter writer) - { - Guard.NotNull(writer, nameof(writer)); - - if (traces.Count > 0) - { - writer.WriteObject("profiler", p => - { - foreach (var kvp in traces) - { - p.WriteObject(kvp.Key, kvp.Value, (value, k) => k - .WriteProperty("elapsedMsTotal", value.Total) - .WriteProperty("elapsedMsAvg", value.Total / value.Count) - .WriteProperty("count", value.Count)); - } - }); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/ProfilerSpan.cs b/src/Squidex.Infrastructure/Log/ProfilerSpan.cs deleted file mode 100644 index 9a2819ca2..000000000 --- a/src/Squidex.Infrastructure/Log/ProfilerSpan.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Log -{ - public sealed class ProfilerSpan : IDisposable - { - private readonly ProfilerSession session; - private readonly string key; - private ValueStopwatch watch = ValueStopwatch.StartNew(); - private List hooks; - - public string Key - { - get { return key; } - } - - public ProfilerSpan(ProfilerSession session, string key) - { - this.session = session; - - this.key = key; - } - - public void Listen(IDisposable hook) - { - Guard.NotNull(hook, nameof(hook)); - - if (hooks == null) - { - hooks = new List(1); - } - - hooks.Add(hook); - } - - public void Dispose() - { - var elapsedMs = watch.Stop(); - - session.Measured(key, elapsedMs); - - if (hooks != null) - { - for (var i = 0; i < hooks.Count; i++) - { - try - { - hooks[i].Dispose(); - } - catch - { - continue; - } - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Log/SemanticLog.cs b/src/Squidex.Infrastructure/Log/SemanticLog.cs deleted file mode 100644 index 3a040a231..000000000 --- a/src/Squidex.Infrastructure/Log/SemanticLog.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// 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; - -namespace Squidex.Infrastructure.Log -{ - public sealed class SemanticLog : ISemanticLog - { - private readonly ILogChannel[] channels; - private readonly ILogAppender[] appenders; - private readonly IObjectWriterFactory writerFactory; - - public SemanticLog( - IEnumerable channels, - IEnumerable appenders, - IObjectWriterFactory writerFactory) - { - Guard.NotNull(channels, nameof(channels)); - Guard.NotNull(appenders, nameof(appenders)); - Guard.NotNull(writerFactory, nameof(writerFactory)); - - this.channels = channels.ToArray(); - this.appenders = appenders.ToArray(); - this.writerFactory = writerFactory; - } - - public void Log(SemanticLogLevel logLevel, T context, Action action) - { - Guard.NotNull(action, nameof(action)); - - var formattedText = FormatText(logLevel, context, action); - - LogFormattedText(logLevel, formattedText); - } - - private void LogFormattedText(SemanticLogLevel logLevel, string formattedText) - { - List exceptions = null; - - for (var i = 0; i < channels.Length; i++) - { - try - { - channels[i].Log(logLevel, formattedText); - } - catch (Exception ex) - { - if (exceptions == null) - { - exceptions = new List(); - } - - exceptions.Add(ex); - } - } - - if (exceptions != null && exceptions.Count > 0) - { - throw new AggregateException("An error occurred while writing to logger(s).", exceptions); - } - } - - private string FormatText(SemanticLogLevel logLevel, T context, Action objectWriter) - { - var writer = writerFactory.Create(); - - try - { - writer.WriteProperty(nameof(logLevel), logLevel.ToString()); - - objectWriter(context, writer); - - for (var i = 0; i < appenders.Length; i++) - { - appenders[i].Append(writer, logLevel); - } - - return writer.ToString(); - } - finally - { - writerFactory.Release(writer); - } - } - - public ISemanticLog CreateScope(Action objectWriter) - { - return new SemanticLog(channels, appenders.Union(new ILogAppender[] { new ConstantsLogWriter(objectWriter) }).ToArray(), writerFactory); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs b/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs deleted file mode 100644 index 1b58a9d02..000000000 --- a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Log -{ - public static class SemanticLogExtensions - { - public static void LogTrace(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Trace, null, (_, w) => objectWriter(w)); - } - - public static void LogTrace(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Trace, context, objectWriter); - } - - public static void LogDebug(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Debug, null, (_, w) => objectWriter(w)); - } - - public static void LogDebug(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Debug, context, objectWriter); - } - - public static void LogInformation(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Information, null, (_, w) => objectWriter(w)); - } - - public static void LogInformation(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Information, context, objectWriter); - } - - public static void LogWarning(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Warning, null, (_, w) => objectWriter(w)); - } - - public static void LogWarning(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Warning, context, objectWriter); - } - - public static void LogWarning(this ISemanticLog log, Exception exception, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Warning, null, (_, w) => w.WriteException(exception, objectWriter)); - } - - public static void LogWarning(this ISemanticLog log, Exception exception, T context, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Warning, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); - } - - public static void LogError(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Error, null, (_, w) => objectWriter(w)); - } - - public static void LogError(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Error, context, objectWriter); - } - - public static void LogError(this ISemanticLog log, Exception exception, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Error, null, (_, w) => w.WriteException(exception, objectWriter)); - } - - public static void LogError(this ISemanticLog log, Exception exception, T context, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Error, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); - } - - public static void LogFatal(this ISemanticLog log, Action objectWriter) - { - log.Log(SemanticLogLevel.Fatal, null, (_, w) => objectWriter(w)); - } - - public static void LogFatal(this ISemanticLog log, T context, Action objectWriter) - { - log.Log(SemanticLogLevel.Fatal, context, objectWriter); - } - - public static void LogFatal(this ISemanticLog log, Exception exception, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Fatal, null, (_, w) => w.WriteException(exception, objectWriter)); - } - - public static void LogFatal(this ISemanticLog log, Exception exception, T context, Action objectWriter = null) - { - log.Log(SemanticLogLevel.Fatal, context, (ctx, w) => w.WriteException(exception, ctx, objectWriter)); - } - - private static void WriteException(this IObjectWriter writer, Exception exception, Action objectWriter) - { - objectWriter?.Invoke(writer); - - if (exception != null) - { - writer.WriteException(exception); - } - } - - private static void WriteException(this IObjectWriter writer, Exception exception, T context, Action objectWriter) - { - objectWriter?.Invoke(context, writer); - - if (exception != null) - { - writer.WriteException(exception); - } - } - - public static IObjectWriter WriteException(this IObjectWriter writer, Exception exception) - { - return writer.WriteObject(nameof(exception), exception, (ctx, w) => - { - w.WriteProperty("type", ctx.GetType().FullName); - - if (ctx.Message != null) - { - w.WriteProperty("message", ctx.Message); - } - - if (ctx.StackTrace != null) - { - w.WriteProperty("stackTrace", ctx.StackTrace); - } - }); - } - - public static IDisposable MeasureTrace(this ISemanticLog log, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Trace, null, (_, w) => objectWriter(w)); - } - - public static IDisposable MeasureTrace(this ISemanticLog log, T context, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Trace, context, objectWriter); - } - - public static IDisposable MeasureDebug(this ISemanticLog log, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Debug, null, (_, w) => objectWriter(w)); - } - - public static IDisposable MeasureDebug(this ISemanticLog log, T context, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Debug, context, objectWriter); - } - - public static IDisposable MeasureInformation(this ISemanticLog log, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Information, null, (_, w) => objectWriter(w)); - } - - public static IDisposable MeasureInformation(this ISemanticLog log, T context, Action objectWriter) - { - return log.Measure(SemanticLogLevel.Information, context, objectWriter); - } - - private static IDisposable Measure(this ISemanticLog log, SemanticLogLevel logLevel, T context, Action objectWriter) - { - var watch = ValueStopwatch.StartNew(); - - return new DelegateDisposable(() => - { - var elapsedMs = watch.Stop(); - - log.Log(logLevel, (Context: context, elapsedMs), (ctx, w) => - { - objectWriter?.Invoke(ctx.Context, w); - - w.WriteProperty("elapsedMs", elapsedMs); - }); - }); - } - } -} diff --git a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs deleted file mode 100644 index 243be67d0..000000000 --- a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; - -namespace Squidex.Infrastructure.Log -{ - public sealed class TimestampLogAppender : ILogAppender - { - private readonly IClock clock; - - public TimestampLogAppender(IClock clock = null) - { - this.clock = clock ?? SystemClock.Instance; - } - - public void Append(IObjectWriter writer, SemanticLogLevel logLevel) - { - writer.WriteProperty("timestamp", clock.GetCurrentInstant()); - } - } -} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs b/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs deleted file mode 100644 index 3992f953e..000000000 --- a/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Migrations -{ - public interface IMigrationPath - { - (int Version, IEnumerable Migrations) GetNext(int version); - } -} diff --git a/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs b/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs deleted file mode 100644 index 2b5253289..000000000 --- a/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.Migrations -{ - [Serializable] - public class MigrationFailedException : Exception - { - public string Name { get; } - - public MigrationFailedException(string name) - : base(FormatException(name)) - { - Name = name; - } - - public MigrationFailedException(string name, Exception inner) - : base(FormatException(name), inner) - { - Name = name; - } - - protected MigrationFailedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Name = info.GetString(nameof(Name)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Name), Name); - } - - private static string FormatException(string name) - { - return $"Failed to run migration '{name}'"; - } - } -} diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs deleted file mode 100644 index 0748e5a1a..000000000 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Migrations -{ - public sealed class Migrator - { - private readonly ISemanticLog log; - private readonly IMigrationStatus migrationStatus; - private readonly IMigrationPath migrationPath; - - public int LockWaitMs { get; set; } = 500; - - public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, ISemanticLog log) - { - Guard.NotNull(migrationStatus, nameof(migrationStatus)); - Guard.NotNull(migrationPath, nameof(migrationPath)); - Guard.NotNull(log, nameof(log)); - - this.migrationStatus = migrationStatus; - this.migrationPath = migrationPath; - - this.log = log; - } - - public async Task MigrateAsync(CancellationToken ct = default) - { - var version = 0; - - try - { - while (!await migrationStatus.TryLockAsync()) - { - log.LogInformation(w => w - .WriteProperty("action", "Migrate") - .WriteProperty("mesage", $"Waiting {LockWaitMs}ms to acquire lock.")); - - await Task.Delay(LockWaitMs); - } - - version = await migrationStatus.GetVersionAsync(); - - while (!ct.IsCancellationRequested) - { - var (newVersion, migrations) = migrationPath.GetNext(version); - - if (migrations == null || !migrations.Any()) - { - break; - } - - foreach (var migration in migrations) - { - var name = migration.GetType().ToString(); - - log.LogInformation(w => w - .WriteProperty("action", "Migration") - .WriteProperty("status", "Started") - .WriteProperty("migrator", name)); - - try - { - using (log.MeasureInformation(w => w - .WriteProperty("action", "Migration") - .WriteProperty("status", "Completed") - .WriteProperty("migrator", name))) - { - await migration.UpdateAsync(); - } - } - catch (Exception ex) - { - log.LogFatal(ex, w => w - .WriteProperty("action", "Migration") - .WriteProperty("status", "Failed") - .WriteProperty("migrator", name)); - - throw new MigrationFailedException(name, ex); - } - } - - version = newVersion; - } - } - finally - { - await migrationStatus.UnlockAsync(version); - } - } - } -} diff --git a/src/Squidex.Infrastructure/NamedId.cs b/src/Squidex.Infrastructure/NamedId.cs deleted file mode 100644 index e0c8106be..000000000 --- a/src/Squidex.Infrastructure/NamedId.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure -{ - public static class NamedId - { - public static NamedId Of(T id, string name) - { - return new NamedId(id, name); - } - } -} diff --git a/src/Squidex.Infrastructure/NamedId{T}.cs b/src/Squidex.Infrastructure/NamedId{T}.cs deleted file mode 100644 index eeeab8ab9..000000000 --- a/src/Squidex.Infrastructure/NamedId{T}.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure -{ - public delegate bool Parser(string input, out T result); - - public sealed class NamedId : IEquatable> - { - private static readonly int GuidLength = Guid.Empty.ToString().Length; - - public T Id { get; } - - public string Name { get; } - - public NamedId(T id, string name) - { - Guard.NotNull(id, nameof(id)); - Guard.NotNull(name, nameof(name)); - - Id = id; - - Name = name; - } - - public override string ToString() - { - return $"{Id},{Name}"; - } - - public override bool Equals(object obj) - { - return Equals(obj as NamedId); - } - - public bool Equals(NamedId other) - { - return other != null && (ReferenceEquals(this, other) || (Id.Equals(other.Id) && Name.Equals(other.Name))); - } - - public override int GetHashCode() - { - return (Id.GetHashCode() * 397) ^ Name.GetHashCode(); - } - - public static bool TryParse(string value, Parser parser, out NamedId result) - { - if (value != null) - { - if (typeof(T) == typeof(Guid)) - { - if (value.Length > GuidLength + 1 && value[GuidLength] == ',') - { - if (parser(value.Substring(0, GuidLength), out var id)) - { - result = new NamedId(id, value.Substring(GuidLength + 1)); - - return true; - } - } - } - else - { - var index = value.IndexOf(','); - - if (index > 0 && index < value.Length - 1) - { - if (parser(value.Substring(0, index), out var id)) - { - result = new NamedId(id, value.Substring(index + 1)); - - return true; - } - } - } - } - - result = null; - - return false; - } - - public static NamedId Parse(string value, Parser parser) - { - if (!TryParse(value, parser, out var result)) - { - throw new ArgumentException("Named id must have at least 2 parts divided by commata.", nameof(value)); - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/None.cs b/src/Squidex.Infrastructure/None.cs deleted file mode 100644 index 76fe93d4a..000000000 --- a/src/Squidex.Infrastructure/None.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure -{ - public sealed class None - { - public static readonly Type Type = typeof(None); - - private None() - { - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs b/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs deleted file mode 100644 index d3aff54d9..000000000 --- a/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ActivationLimit : IActivationLimit, IDeactivater - { - private readonly IGrainActivationContext context; - private readonly IActivationLimiter limiter; - private int maxActivations; - - public ActivationLimit(IGrainActivationContext context, IActivationLimiter limiter) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(limiter, nameof(limiter)); - - this.context = context; - this.limiter = limiter; - } - - public void ReportIAmAlive() - { - if (maxActivations > 0) - { - limiter.Register(context.GrainType, this, maxActivations); - } - } - - public void ReportIAmDead() - { - if (maxActivations > 0) - { - limiter.Unregister(context.GrainType, this); - } - } - - public void SetLimit(int activations, TimeSpan lifetime) - { - maxActivations = activations; - - context.ObservableLifecycle?.Subscribe("Limiter", GrainLifecycleStage.Activate, - ct => - { - var runtime = context.ActivationServices.GetRequiredService(); - - runtime.DelayDeactivation(context.GrainInstance, lifetime); - - ReportIAmAlive(); - - return TaskHelper.Done; - }, - ct => - { - ReportIAmDead(); - - return TaskHelper.Done; - }); - } - - void IDeactivater.Deactivate() - { - var runtime = context.ActivationServices.GetRequiredService(); - - runtime.DeactivateOnIdle(context.GrainInstance); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Orleans/GrainBase.cs b/src/Squidex.Infrastructure/Orleans/GrainBase.cs deleted file mode 100644 index 9ae6b7d76..000000000 --- a/src/Squidex.Infrastructure/Orleans/GrainBase.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Core; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public abstract class GrainBase : Grain - { - protected GrainBase() - { - } - - protected GrainBase(IGrainIdentity identity, IGrainRuntime runtime) - : base(identity, runtime) - { - } - - public void ReportIAmAlive() - { - var limit = ServiceProvider.GetService(); - - limit?.ReportIAmAlive(); - } - - public void ReportIAmDead() - { - var limit = ServiceProvider.GetService(); - - limit?.ReportIAmDead(); - } - - protected void TryDelayDeactivation(TimeSpan timeSpan) - { - try - { - DelayDeactivation(timeSpan); - } - catch (InvalidOperationException) - { - } - } - - protected void TryDeactivateOnIdle() - { - try - { - DeactivateOnIdle(); - } - catch (InvalidOperationException) - { - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs deleted file mode 100644 index cebe9623f..000000000 --- a/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain - { - private const int NumTries = 10; - private readonly IGrainFactory grainFactory; - - public GrainBootstrap(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task StartAsync(CancellationToken ct = default) - { - for (var i = 1; i <= NumTries; i++) - { - ct.ThrowIfCancellationRequested(); - try - { - var grain = grainFactory.GetGrain(SingleGrain.Id); - - await grain.ActivateAsync(); - - return; - } - catch (OrleansException) - { - if (i == NumTries) - { - throw; - } - } - } - } - - public override string ToString() - { - return typeof(T).ToString(); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/GrainState.cs b/src/Squidex.Infrastructure/Orleans/GrainState.cs deleted file mode 100644 index d688acabd..000000000 --- a/src/Squidex.Infrastructure/Orleans/GrainState.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// 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 Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class GrainState : IGrainState where T : class, new() - { - private readonly IGrainActivationContext context; - private IPersistence persistence; - - public T Value { get; set; } = new T(); - - public long Version - { - get { return persistence.Version; } - } - - public GrainState(IGrainActivationContext context) - { - Guard.NotNull(context, nameof(context)); - - this.context = context; - - context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); - } - - public Task SetupAsync(CancellationToken ct = default) - { - if (ct.IsCancellationRequested) - { - return Task.CompletedTask; - } - - if (context.GrainIdentity.PrimaryKeyString != null) - { - var store = context.ActivationServices.GetService>(); - - persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKeyString, ApplyState); - } - else - { - var store = context.ActivationServices.GetService>(); - - persistence = store.WithSnapshots(GetType(), context.GrainIdentity.PrimaryKey, ApplyState); - } - - return persistence.ReadAsync(); - } - - private void ApplyState(T value) - { - Value = value; - } - - public Task ClearAsync() - { - Value = new T(); - - return persistence.DeleteAsync(); - } - - public Task WriteAsync() - { - return persistence.WriteSnapshotAsync(Value); - } - - public Task WriteEventAsync(Envelope envelope) - { - return persistence.WriteEventAsync(envelope); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/ILockGrain.cs b/src/Squidex.Infrastructure/Orleans/ILockGrain.cs deleted file mode 100644 index 05b4f18c0..000000000 --- a/src/Squidex.Infrastructure/Orleans/ILockGrain.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Orleans -{ - public interface ILockGrain : IGrainWithStringKey - { - Task AcquireLockAsync(string key); - - Task ReleaseLockAsync(string releaseToken); - } -} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs deleted file mode 100644 index bca54d1a2..000000000 --- a/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public interface IUniqueNameIndexGrain - { - Task ReserveAsync(T id, string name); - - Task AddAsync(string token); - - Task CountAsync(); - - Task RemoveReservationAsync(string token); - - Task RemoveAsync(T id); - - Task RebuildAsync(Dictionary values); - - Task ClearAsync(); - - Task GetIdAsync(string name); - - Task> GetIdsAsync(string[] names); - - Task> GetIdsAsync(); - } -} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs deleted file mode 100644 index fdae2ceb2..000000000 --- a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class IdsIndexGrain : Grain, IIdsIndexGrain where TState : IdsIndexState, new() - { - private readonly IGrainState state; - - public IdsIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task CountAsync() - { - return Task.FromResult(state.Value.Ids.Count); - } - - public Task RebuildAsync(HashSet ids) - { - state.Value = new TState { Ids = ids }; - - return state.WriteAsync(); - } - - public Task AddAsync(T id) - { - state.Value.Ids.Add(id); - - return state.WriteAsync(); - } - - public Task RemoveAsync(T id) - { - state.Value.Ids.Remove(id); - - return state.WriteAsync(); - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task> GetIdsAsync() - { - return Task.FromResult(state.Value.Ids.ToList()); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs deleted file mode 100644 index 5b71e57ca..000000000 --- a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameIndexGrain : Grain, IUniqueNameIndexGrain where TState : UniqueNameIndexState, new() - { - private readonly Dictionary reservations = new Dictionary(); - private readonly IGrainState state; - - public UniqueNameIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task CountAsync() - { - return Task.FromResult(state.Value.Names.Count); - } - - public Task ClearAsync() - { - reservations.Clear(); - - return state.ClearAsync(); - } - - public Task RebuildAsync(Dictionary names) - { - state.Value = new TState { Names = names }; - - return state.WriteAsync(); - } - - public Task ReserveAsync(T id, string name) - { - string token = default; - - if (!IsInUse(name) && !IsReserved(name)) - { - token = RandomHash.Simple(); - - reservations.Add(token, (name, id)); - } - - return Task.FromResult(token); - } - - public async Task AddAsync(string token) - { - if (reservations.TryGetValue(token ?? string.Empty, out var reservation)) - { - state.Value.Names.Add(reservation.Name, reservation.Id); - - await state.WriteAsync(); - - reservations.Remove(token); - - return true; - } - - return false; - } - - public Task RemoveReservationAsync(string token) - { - reservations.Remove(token ?? string.Empty); - - return TaskHelper.Done; - } - - public async Task RemoveAsync(T id) - { - var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key; - - if (name != null) - { - state.Value.Names.Remove(name); - - await state.WriteAsync(); - } - } - - public Task> GetIdsAsync(string[] names) - { - var result = new List(); - - if (names != null) - { - foreach (var name in names) - { - if (state.Value.Names.TryGetValue(name, out var id)) - { - result.Add(id); - } - } - } - - return Task.FromResult(result); - } - - public Task GetIdAsync(string name) - { - state.Value.Names.TryGetValue(name, out var id); - - return Task.FromResult(id); - } - - public Task> GetIdsAsync() - { - return Task.FromResult(state.Value.Names.Values.ToList()); - } - - private bool IsInUse(string name) - { - return state.Value.Names.ContainsKey(name); - } - - private bool IsReserved(string name) - { - return reservations.Values.Any(x => x.Name == name); - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/J{T}.cs b/src/Squidex.Infrastructure/Orleans/J{T}.cs deleted file mode 100644 index 8946ef845..000000000 --- a/src/Squidex.Infrastructure/Orleans/J{T}.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Orleans.CodeGeneration; -using Orleans.Concurrency; -using Orleans.Serialization; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Orleans -{ - [Immutable] - public struct J - { - public T Value { get; } - - public J(T value) - { - Value = value; - } - - public static implicit operator T(J value) - { - return value.Value; - } - - public static implicit operator J(T d) - { - return new J(d); - } - - public override string ToString() - { - return Value?.ToString() ?? string.Empty; - } - - public static Task> AsTask(T value) - { - return Task.FromResult>(value); - } - - [CopierMethod] - public static object Copy(object input, ICopyContext context) - { - return input; - } - - [SerializerMethod] - public static void Serialize(object input, ISerializationContext context, Type expected) - { - using (Profiler.TraceMethod(nameof(J))) - { - var jsonSerializer = GetSerializer(context); - - var stream = new StreamWriterWrapper(context.StreamWriter); - - jsonSerializer.Serialize(input, stream); - } - } - - [DeserializerMethod] - public static object Deserialize(Type expected, IDeserializationContext context) - { - using (Profiler.TraceMethod(nameof(J))) - { - var jsonSerializer = GetSerializer(context); - - var stream = new StreamReaderWrapper(context.StreamReader); - - return jsonSerializer.Deserialize(stream, expected); - } - } - - private static IJsonSerializer GetSerializer(ISerializerContext context) - { - try - { - return context?.ServiceProvider?.GetRequiredService() ?? J.DefaultSerializer; - } - catch - { - return J.DefaultSerializer; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs b/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs deleted file mode 100644 index 842a0dad9..000000000 --- a/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LocalCacheFilter : IIncomingGrainCallFilter - { - private readonly ILocalCache localCache; - - public LocalCacheFilter(ILocalCache localCache) - { - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - if (!context.Grain.GetType().Namespace.StartsWith("Orleans", StringComparison.OrdinalIgnoreCase)) - { - using (localCache.StartContext()) - { - await context.Invoke(); - } - } - else - { - await context.Invoke(); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/LockGrain.cs b/src/Squidex.Infrastructure/Orleans/LockGrain.cs deleted file mode 100644 index 5e57fabaa..000000000 --- a/src/Squidex.Infrastructure/Orleans/LockGrain.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LockGrain : GrainOfString, ILockGrain - { - private readonly Dictionary locks = new Dictionary(); - - public Task AcquireLockAsync(string key) - { - string releaseToken = null; - - if (!locks.ContainsKey(key)) - { - releaseToken = Guid.NewGuid().ToString(); - - locks.Add(key, releaseToken); - } - - return Task.FromResult(releaseToken); - } - - public Task ReleaseLockAsync(string releaseToken) - { - var key = locks.FirstOrDefault(x => x.Value == releaseToken).Key; - - if (!string.IsNullOrWhiteSpace(key)) - { - locks.Remove(key); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs deleted file mode 100644 index b7edc178c..000000000 --- a/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LoggingFilter : IIncomingGrainCallFilter - { - private readonly ISemanticLog log; - - public LoggingFilter(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - this.log = log; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - try - { - await context.Invoke(); - } - catch (DomainException) - { - throw; - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "GrainInvoked") - .WriteProperty("status", "Failed") - .WriteProperty("grain", context.Grain.ToString()) - .WriteProperty("grainMethod", context.ImplementationMethod.ToString())); - - throw; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs b/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs deleted file mode 100644 index 9284b2a9f..000000000 --- a/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Orleans.Serialization; - -namespace Squidex.Infrastructure.Orleans -{ - internal sealed class StreamReaderWrapper : Stream - { - private readonly IBinaryTokenStreamReader reader; - - public override bool CanRead - { - get { return true; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { return reader.Length; } - } - - public override long Position - { - get - { - return reader.CurrentPosition; - } - set - { - throw new NotSupportedException(); - } - } - - public StreamReaderWrapper(IBinaryTokenStreamReader reader) - { - this.reader = reader; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - var bytesLeft = reader.Length - reader.CurrentPosition; - - if (bytesLeft < count) - { - count = bytesLeft; - } - - reader.ReadByteArray(buffer, offset, count); - - return count; - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/src/Squidex.Infrastructure/Plugins/PluginManager.cs deleted file mode 100644 index cdae457ac..000000000 --- a/src/Squidex.Infrastructure/Plugins/PluginManager.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; - -namespace Squidex.Infrastructure.Plugins -{ - public sealed class PluginManager - { - private readonly HashSet loadedPlugins = new HashSet(); - private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); - - public IReadOnlyCollection Plugins - { - get { return loadedPlugins; } - } - - public void Add(string name, Assembly assembly) - { - Guard.NotNull(assembly, nameof(assembly)); - - var pluginTypes = - assembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t)) - .Where(t => !t.IsAbstract); - - foreach (var pluginType in pluginTypes) - { - try - { - var plugin = (IPlugin)Activator.CreateInstance(pluginType); - - loadedPlugins.Add(plugin); - } - catch (Exception ex) - { - LogException(name, "Instantiating", ex); - } - } - } - - public void LogException(string plugin, string action, Exception exception) - { - Guard.NotNull(plugin, nameof(plugin)); - Guard.NotNull(action, nameof(action)); - Guard.NotNull(exception, nameof(exception)); - - exceptions.Add((plugin, action, exception)); - } - - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - Guard.NotNull(services, nameof(services)); - Guard.NotNull(config, nameof(config)); - - foreach (var plugin in loadedPlugins) - { - plugin.ConfigureServices(services, config); - } - } - - public void ConfigureBefore(IApplicationBuilder app) - { - Guard.NotNull(app, nameof(app)); - - foreach (var plugin in loadedPlugins.OfType()) - { - plugin.ConfigureBefore(app); - } - } - - public void ConfigureAfter(IApplicationBuilder app) - { - Guard.NotNull(app, nameof(app)); - - foreach (var plugin in loadedPlugins.OfType()) - { - plugin.ConfigureAfter(app); - } - } - - public void Log(ISemanticLog log) - { - Guard.NotNull(log, nameof(log)); - - if (loadedPlugins.Count > 0 || exceptions.Count > 0) - { - var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; - - log.LogInformation(w => w - .WriteProperty("action", "pluginsLoaded") - .WriteProperty("status", status) - .WriteArray("errors", e => - { - foreach (var error in exceptions) - { - e.WriteObject(x => x - .WriteProperty("plugin", error.Plugin) - .WriteProperty("action", error.Action) - .WriteException(error.Exception)); - } - }) - .WriteArray("plugins", a => - { - foreach (var plugin in loadedPlugins) - { - a.WriteValue(plugin.GetType().ToString()); - } - })); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/src/Squidex.Infrastructure/Queries/ClrFilter.cs deleted file mode 100644 index c2cb7eaa4..000000000 --- a/src/Squidex.Infrastructure/Queries/ClrFilter.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public static class ClrFilter - { - public static LogicalFilter And(params FilterNode[] filters) - { - return new LogicalFilter(LogicalFilterType.And, filters); - } - - public static LogicalFilter Or(params FilterNode[] filters) - { - return new LogicalFilter(LogicalFilterType.Or, filters); - } - - public static NegateFilter Not(FilterNode filter) - { - return new NegateFilter(filter); - } - - public static CompareFilter Eq(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Equals, value); - } - - public static CompareFilter Ne(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.NotEquals, value); - } - - public static CompareFilter Lt(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.LessThan, value); - } - - public static CompareFilter Le(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.LessThanOrEqual, value); - } - - public static CompareFilter Gt(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.GreaterThan, value); - } - - public static CompareFilter Ge(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.GreaterThanOrEqual, value); - } - - public static CompareFilter Contains(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Contains, value); - } - - public static CompareFilter EndsWith(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.EndsWith, value); - } - - public static CompareFilter StartsWith(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.StartsWith, value); - } - - public static CompareFilter Empty(PropertyPath path) - { - return Binary(path, CompareOperator.Empty, null); - } - - public static CompareFilter In(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.In, value); - } - - private static CompareFilter Binary(PropertyPath path, CompareOperator @operator, ClrValue value) - { - return new CompareFilter(path, @operator, value ?? ClrValue.Null); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/ClrValue.cs b/src/Squidex.Infrastructure/Queries/ClrValue.cs deleted file mode 100644 index 1a0719486..000000000 --- a/src/Squidex.Infrastructure/Queries/ClrValue.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using NodaTime; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class ClrValue - { - public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); - - public object Value { get; } - - public ClrValueType ValueType { get; } - - public bool IsList { get; } - - private ClrValue(object value, ClrValueType valueType, bool isList) - { - Value = value; - ValueType = valueType; - - IsList = isList; - } - - public static implicit operator ClrValue(Instant value) - { - return new ClrValue(value, ClrValueType.Instant, false); - } - - public static implicit operator ClrValue(Guid value) - { - return new ClrValue(value, ClrValueType.Guid, false); - } - - public static implicit operator ClrValue(bool value) - { - return new ClrValue(value, ClrValueType.Boolean, false); - } - - public static implicit operator ClrValue(float value) - { - return new ClrValue(value, ClrValueType.Single, false); - } - - public static implicit operator ClrValue(double value) - { - return new ClrValue(value, ClrValueType.Double, false); - } - - public static implicit operator ClrValue(int value) - { - return new ClrValue(value, ClrValueType.Int32, false); - } - - public static implicit operator ClrValue(long value) - { - return new ClrValue(value, ClrValueType.Int64, false); - } - - public static implicit operator ClrValue(string value) - { - return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; - } - - public static implicit operator ClrValue(List value) - { - return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; - } - - public override string ToString() - { - if (Value is IList list) - { - return $"[{string.Join(", ", list.OfType().Select(ToString).ToArray())}]"; - } - - return ToString(Value); - } - - private static string ToString(object value) - { - if (value == null) - { - return "null"; - } - - if (value is string s) - { - return $"'{s.Replace("'", "\\'")}'"; - } - - return string.Format(CultureInfo.InvariantCulture, "{0}", value); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/src/Squidex.Infrastructure/Queries/CompareFilter.cs deleted file mode 100644 index 5b522e6c9..000000000 --- a/src/Squidex.Infrastructure/Queries/CompareFilter.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public sealed class CompareFilter : FilterNode - { - public PropertyPath Path { get; } - - public CompareOperator Operator { get; } - - public TValue Value { get; } - - public CompareFilter(PropertyPath path, CompareOperator @operator, TValue value) - { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(value, nameof(value)); - Guard.Enum(@operator, nameof(@operator)); - - Path = path; - - Operator = @operator; - - Value = value; - } - - public override T Accept(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - switch (Operator) - { - case CompareOperator.Contains: - return $"contains({Path}, {Value})"; - case CompareOperator.Empty: - return $"empty({Path})"; - case CompareOperator.EndsWith: - return $"endsWith({Path}, {Value})"; - case CompareOperator.StartsWith: - return $"startsWith({Path}, {Value})"; - case CompareOperator.Equals: - return $"{Path} == {Value}"; - case CompareOperator.NotEquals: - return $"{Path} != {Value}"; - case CompareOperator.GreaterThan: - return $"{Path} > {Value}"; - case CompareOperator.GreaterThanOrEqual: - return $"{Path} >= {Value}"; - case CompareOperator.LessThan: - return $"{Path} < {Value}"; - case CompareOperator.LessThanOrEqual: - return $"{Path} <= {Value}"; - case CompareOperator.In: - return $"{Path} in {Value}"; - default: - return string.Empty; - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/FilterNode.cs b/src/Squidex.Infrastructure/Queries/FilterNode.cs deleted file mode 100644 index 8dd348ce7..000000000 --- a/src/Squidex.Infrastructure/Queries/FilterNode.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public abstract class FilterNode - { - public abstract T Accept(FilterNodeVisitor visitor); - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs b/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs deleted file mode 100644 index e54883014..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure.Queries.Json -{ - public sealed class FilterConverter : JsonClassConverter> - { - public override IEnumerable SupportedTypes - { - get - { - yield return typeof(CompareFilter); - yield return typeof(FilterNode); - yield return typeof(LogicalFilter); - yield return typeof(NegateFilter); - } - } - - public override bool CanConvert(Type objectType) - { - return SupportedTypes.Contains(objectType); - } - - protected override FilterNode ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) - { - if (reader.TokenType != JsonToken.StartObject) - { - throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); - } - - FilterNode result = null; - - var comparePath = (PropertyPath)null; - var compareOperator = (CompareOperator)99; - var compareValue = (IJsonValue)null; - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - var propertyName = reader.Value.ToString(); - - if (!reader.Read()) - { - throw new JsonSerializationException("Unexpected end when reading filter."); - } - - if (result != null) - { - throw new JsonSerializationException($"Unexpected property {propertyName}"); - } - - switch (propertyName.ToLowerInvariant()) - { - case "not": - var filter = serializer.Deserialize>(reader); - - result = new NegateFilter(filter); - break; - case "and": - var andFilters = serializer.Deserialize>>(reader); - - result = new LogicalFilter(LogicalFilterType.And, andFilters); - break; - case "or": - var orFilters = serializer.Deserialize>>(reader); - - result = new LogicalFilter(LogicalFilterType.Or, orFilters); - break; - case "path": - comparePath = serializer.Deserialize(reader); - break; - case "op": - compareOperator = ReadOperator(reader, serializer); - break; - case "value": - compareValue = serializer.Deserialize(reader); - break; - } - - break; - case JsonToken.Comment: - break; - case JsonToken.EndObject: - if (result != null) - { - return result; - } - - if (comparePath == null) - { - throw new JsonSerializationException("Path not defined."); - } - - if (compareValue == null && compareOperator != CompareOperator.Empty) - { - throw new JsonSerializationException("Value not defined."); - } - - if (!compareOperator.IsEnumValue()) - { - throw new JsonSerializationException("Operator not defined."); - } - - return new CompareFilter(comparePath, compareOperator, compareValue ?? JsonValue.Null); - } - } - - throw new JsonSerializationException("Unexpected end when reading filter."); - } - - private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer) - { - var value = serializer.Deserialize(reader); - - switch (value.ToLowerInvariant()) - { - case "eq": - return CompareOperator.Equals; - case "ne": - return CompareOperator.NotEquals; - case "lt": - return CompareOperator.LessThan; - case "le": - return CompareOperator.LessThanOrEqual; - case "gt": - return CompareOperator.GreaterThan; - case "ge": - return CompareOperator.GreaterThanOrEqual; - case "empty": - return CompareOperator.Empty; - case "contains": - return CompareOperator.Contains; - case "endswith": - return CompareOperator.EndsWith; - case "startswith": - return CompareOperator.StartsWith; - case "in": - return CompareOperator.In; - } - - throw new JsonSerializationException($"Unexpected compare operator, got {value}."); - } - - protected override void WriteValue(JsonWriter writer, FilterNode value, JsonSerializer serializer) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs b/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs deleted file mode 100644 index c6a506e08..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using NJsonSchema; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Infrastructure.Queries.Json -{ - public sealed class JsonFilterVisitor : FilterNodeVisitor, IJsonValue> - { - private readonly List errors; - private readonly JsonSchema schema; - - private JsonFilterVisitor(JsonSchema schema, List errors) - { - this.schema = schema; - - this.errors = errors; - } - - public static FilterNode Parse(FilterNode filter, JsonSchema schema, List errors) - { - var visitor = new JsonFilterVisitor(schema, errors); - - var parsed = filter.Accept(visitor); - - if (visitor.errors.Count > 0) - { - return null; - } - else - { - return parsed; - } - } - - public override FilterNode Visit(NegateFilter nodeIn) - { - return new NegateFilter(nodeIn.Accept(this)); - } - - public override FilterNode Visit(LogicalFilter nodeIn) - { - return new LogicalFilter(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - CompareFilter result = null; - - if (nodeIn.Path.TryGetProperty(schema, errors, out var property)) - { - var isValidOperator = OperatorValidator.IsAllowedOperator(property, nodeIn.Operator); - - if (!isValidOperator) - { - errors.Add($"{nodeIn.Operator} is not a valid operator for type {property.Type} at {nodeIn.Path}."); - } - - var value = ValueConverter.Convert(property, nodeIn.Value, nodeIn.Path, errors); - - if (value != null && isValidOperator) - { - if (value.IsList && nodeIn.Operator != CompareOperator.In) - { - errors.Add($"Array value is not allowed for '{nodeIn.Operator}' operator and path '{nodeIn.Path}'."); - } - - result = new CompareFilter(nodeIn.Path, nodeIn.Operator, value); - } - } - - result = result ?? new CompareFilter(nodeIn.Path, nodeIn.Operator, ClrValue.Null); - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs b/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs deleted file mode 100644 index 2062f310d..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using NJsonSchema; - -namespace Squidex.Infrastructure.Queries.Json -{ - public static class PropertyPathValidator - { - public static bool TryGetProperty(this PropertyPath path, JsonSchema schema, List errors, out JsonSchema property) - { - foreach (var element in path) - { - var parent = schema.Reference ?? schema; - - if (parent.Properties.TryGetValue(element, out var p)) - { - schema = p; - } - else - { - if (!string.IsNullOrWhiteSpace(parent.Title)) - { - errors.Add($"'{element}' is not a property of '{parent.Title}'."); - } - else - { - errors.Add($"Path '{path}' does not point to a valid property in the model."); - } - - property = null; - - return false; - } - } - - property = schema; - - return true; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs deleted file mode 100644 index afd17c24b..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using NJsonSchema; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Infrastructure.Queries.Json -{ - public static class QueryParser - { - public static ClrQuery Parse(this JsonSchema schema, string json, IJsonSerializer jsonSerializer) - { - var query = ParseFromJson(json, jsonSerializer); - - var result = SimpleMapper.Map(query, new ClrQuery()); - - var errors = new List(); - - ConvertSorting(schema, result, errors); - ConvertFilters(schema, result, errors, query); - - if (errors.Count > 0) - { - throw new ValidationException("Failed to parse json query", errors.Select(x => new ValidationError(x)).ToArray()); - } - - return result; - } - - private static void ConvertFilters(JsonSchema schema, ClrQuery result, List errors, Query query) - { - if (query.Filter != null) - { - var filter = JsonFilterVisitor.Parse(query.Filter, schema, errors); - - result.Filter = Optimizer.Optimize(filter); - } - } - - private static void ConvertSorting(JsonSchema schema, ClrQuery result, List errors) - { - if (result.Sort != null) - { - foreach (var sorting in result.Sort) - { - sorting.Path.TryGetProperty(schema, errors, out _); - } - } - } - - private static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) - { - try - { - return jsonSerializer.Deserialize>(json); - } - catch (JsonException ex) - { - throw new ValidationException("Failed to parse json query.", new ValidationError(ex.Message)); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs deleted file mode 100644 index 19f8812f7..000000000 --- a/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs +++ /dev/null @@ -1,238 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NJsonSchema; -using NodaTime; -using NodaTime.Text; -using Squidex.Infrastructure.Json.Objects; - -namespace Squidex.Infrastructure.Queries.Json -{ - public static class ValueConverter - { - private delegate bool Parser(List errors, PropertyPath path, IJsonValue value, out T result); - - private static readonly InstantPattern[] InstantPatterns = - { - InstantPattern.General, - InstantPattern.ExtendedIso, - InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") - }; - - public static ClrValue Convert(JsonSchema schema, IJsonValue value, PropertyPath path, List errors) - { - ClrValue result = null; - - switch (GetType(schema)) - { - case JsonObjectType.Boolean: - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseBoolean); - } - else if (TryParseBoolean(errors, path, value, out var temp)) - { - result = temp; - } - - break; - } - - case JsonObjectType.Integer: - case JsonObjectType.Number: - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseNumber); - } - else if (TryParseNumber(errors, path, value, out var temp)) - { - result = temp; - } - - break; - } - - case JsonObjectType.String: - { - if (schema.Format == JsonFormatStrings.Guid) - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseGuid); - } - else if (TryParseGuid(errors, path, value, out var temp)) - { - result = temp; - } - } - else if (schema.Format == JsonFormatStrings.DateTime) - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseDateTime); - } - else if (TryParseDateTime(errors, path, value, out var temp)) - { - result = temp; - } - } - else - { - if (value is JsonArray jsonArray) - { - result = ParseArray(errors, path, jsonArray, TryParseString); - } - else if (TryParseString(errors, path, value, out var temp)) - { - result = temp; - } - } - - break; - } - - default: - { - errors.Add($"Unsupported type {schema.Type} for {path}."); - break; - } - } - - return result; - } - - private static List ParseArray(List errors, PropertyPath path, JsonArray array, Parser parser) - { - var items = new List(); - - foreach (var item in array) - { - if (parser(errors, path, item, out var temp)) - { - items.Add(temp); - } - } - - return items; - } - - private static bool TryParseBoolean(List errors, PropertyPath path, IJsonValue value, out bool result) - { - result = default; - - if (value is JsonBoolean jsonBoolean) - { - result = jsonBoolean.Value; - - return true; - } - - errors.Add($"Expected Boolean for path '{path}', but got {value.Type}."); - - return false; - } - - private static bool TryParseNumber(List errors, PropertyPath path, IJsonValue value, out double result) - { - result = default; - - if (value is JsonNumber jsonNumber) - { - result = jsonNumber.Value; - - return true; - } - - errors.Add($"Expected Number for path '{path}', but got {value.Type}."); - - return false; - } - - private static bool TryParseString(List errors, PropertyPath path, IJsonValue value, out string result) - { - result = default; - - if (value is JsonString jsonString) - { - result = jsonString.Value; - - return true; - } - else if (value is JsonNull) - { - return true; - } - - errors.Add($"Expected String for path '{path}', but got {value.Type}."); - - return false; - } - - private static bool TryParseGuid(List errors, PropertyPath path, IJsonValue value, out Guid result) - { - result = default; - - if (value is JsonString jsonString) - { - if (Guid.TryParse(jsonString.Value, out result)) - { - return true; - } - - errors.Add($"Expected Guid String for path '{path}', but got invalid String."); - } - else - { - errors.Add($"Expected Guid String for path '{path}', but got {value.Type}."); - } - - return false; - } - - private static bool TryParseDateTime(List errors, PropertyPath path, IJsonValue value, out Instant result) - { - result = default; - - if (value is JsonString jsonString) - { - foreach (var pattern in InstantPatterns) - { - var parsed = pattern.Parse(jsonString.Value); - - if (parsed.Success) - { - result = parsed.Value; - - return true; - } - } - - errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got invalid String."); - } - else - { - errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got {value.Type}."); - } - - return false; - } - - private static JsonObjectType GetType(JsonSchema schema) - { - if (schema.Item != null) - { - return schema.Item.Type; - } - - return schema.Type; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/LogicalFilter.cs b/src/Squidex.Infrastructure/Queries/LogicalFilter.cs deleted file mode 100644 index 1fc2a1416..000000000 --- a/src/Squidex.Infrastructure/Queries/LogicalFilter.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class LogicalFilter : FilterNode - { - public IReadOnlyList> Filters { get; } - - public LogicalFilterType Type { get; } - - public LogicalFilter(LogicalFilterType type, IReadOnlyList> filters) - { - Guard.NotNull(filters, nameof(filters)); - Guard.Enum(type, nameof(type)); - - Filters = filters; - - Type = type; - } - - public override T Accept(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/NegateFilter.cs b/src/Squidex.Infrastructure/Queries/NegateFilter.cs deleted file mode 100644 index 09583a0f8..000000000 --- a/src/Squidex.Infrastructure/Queries/NegateFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public sealed class NegateFilter : FilterNode - { - public FilterNode Filter { get; } - - public NegateFilter(FilterNode filter) - { - Guard.NotNull(filter, nameof(filter)); - - Filter = filter; - } - - public override T Accept(FilterNodeVisitor visitor) - { - return visitor.Visit(this); - } - - public override string ToString() - { - return $"!({Filter})"; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs deleted file mode 100644 index db3fb47b0..000000000 --- a/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.OData; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; -using NodaTime; -using NodaTime.Text; - -namespace Squidex.Infrastructure.Queries.OData -{ - public sealed class ConstantWithTypeVisitor : QueryNodeVisitor - { - private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); - private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); - private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); - private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); - private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); - private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); - private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); - - private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); - - private ConstantWithTypeVisitor() - { - } - - public static ClrValue Visit(QueryNode node) - { - return node.Accept(Instance); - } - - public override ClrValue Visit(ConvertNode nodeIn) - { - if (nodeIn.TypeReference.Definition == BooleanType) - { - var value = ConstantVisitor.Visit(nodeIn.Source); - - return bool.Parse(value.ToString()); - } - - if (nodeIn.TypeReference.Definition == GuidType) - { - var value = ConstantVisitor.Visit(nodeIn.Source); - - return Guid.Parse(value.ToString()); - } - - if (nodeIn.TypeReference.Definition == DateTimeType) - { - var value = ConstantVisitor.Visit(nodeIn.Source); - - return ParseInstant(value); - } - - if (ConstantVisitor.Visit(nodeIn.Source) == null) - { - return ClrValue.Null; - } - - throw new NotSupportedException(); - } - - public override ClrValue Visit(CollectionConstantNode nodeIn) - { - if (nodeIn.ItemType.Definition == DateTimeType) - { - return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList(); - } - - if (nodeIn.ItemType.Definition == GuidType) - { - return nodeIn.Collection.Select(x => (Guid)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == BooleanType) - { - return nodeIn.Collection.Select(x => (bool)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == SingleType) - { - return nodeIn.Collection.Select(x => (float)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == DoubleType) - { - return nodeIn.Collection.Select(x => (double)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == Int32Type) - { - return nodeIn.Collection.Select(x => (int)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == Int64Type) - { - return nodeIn.Collection.Select(x => (long)x.Value).ToList(); - } - - if (nodeIn.ItemType.Definition == StringType) - { - return nodeIn.Collection.Select(x => (string)x.Value).ToList(); - } - - throw new NotSupportedException(); - } - - public override ClrValue Visit(ConstantNode nodeIn) - { - if (nodeIn.TypeReference.Definition == BooleanType) - { - return (bool)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == SingleType) - { - return (float)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == DoubleType) - { - return (double)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == Int32Type) - { - return (int)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == Int64Type) - { - return (long)nodeIn.Value; - } - - if (nodeIn.TypeReference.Definition == StringType) - { - return (string)nodeIn.Value; - } - - throw new NotSupportedException(); - } - - private static Instant ParseInstant(object value) - { - if (value is DateTimeOffset dateTimeOffset) - { - return Instant.FromDateTimeOffset(dateTimeOffset.Add(dateTimeOffset.Offset)); - } - - if (value is DateTime dateTime) - { - return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); - } - - if (value is Date date) - { - return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); - } - - var parseResult = InstantPattern.General.Parse(value.ToString()); - - if (!parseResult.Success) - { - throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); - } - - return parseResult.Value; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs deleted file mode 100644 index 47a5d0ed0..000000000 --- a/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; - -namespace Squidex.Infrastructure.Queries.OData -{ - public static class EdmModelExtensions - { - static EdmModelExtensions() - { - CustomUriFunctions.AddCustomUriFunction("empty", - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetBoolean(false), - EdmCoreModel.Instance.GetString(true))); - } - - public static ODataUriParser ParseQuery(this IEdmModel model, string query) - { - if (!model.EntityContainer.EntitySets().Any()) - { - return null; - } - - query = query ?? string.Empty; - - var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); - - if (query.StartsWith("?", StringComparison.Ordinal)) - { - query = query.Substring(1); - } - - var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); - - return parser; - } - - public static ClrQuery ToQuery(this ODataUriParser parser) - { - var query = new ClrQuery(); - - if (parser != null) - { - parser.ParseTake(query); - parser.ParseSkip(query); - parser.ParseFilter(query); - parser.ParseSort(query); - } - - return query; - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Optimizer.cs b/src/Squidex.Infrastructure/Queries/Optimizer.cs deleted file mode 100644 index 7a8cb170d..000000000 --- a/src/Squidex.Infrastructure/Queries/Optimizer.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class Optimizer : TransformVisitor - { - private static readonly Optimizer Instance = new Optimizer(); - - private Optimizer() - { - } - - public static FilterNode Optimize(FilterNode source) - { - return source?.Accept(Instance); - } - - public override FilterNode Visit(LogicalFilter nodeIn) - { - var pruned = nodeIn.Filters.Select(x => x.Accept(this)).Where(x => x != null).ToList(); - - if (pruned.Count == 1) - { - return pruned[0]; - } - - if (pruned.Count == 0) - { - return null; - } - - return new LogicalFilter(nodeIn.Type, pruned); - } - - public override FilterNode Visit(NegateFilter nodeIn) - { - var pruned = nodeIn.Filter.Accept(this); - - if (pruned == null) - { - return null; - } - - if (pruned is CompareFilter comparison) - { - if (comparison.Operator == CompareOperator.Equals) - { - return new CompareFilter(comparison.Path, CompareOperator.NotEquals, comparison.Value); - } - - if (comparison.Operator == CompareOperator.NotEquals) - { - return new CompareFilter(comparison.Path, CompareOperator.Equals, comparison.Value); - } - } - - return new NegateFilter(pruned); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs b/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs deleted file mode 100644 index ec10a2452..000000000 --- a/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class PascalCasePathConverter : TransformVisitor - { - private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter(); - - private PascalCasePathConverter() - { - } - - public static FilterNode Transform(FilterNode node) - { - return node.Accept(Instance); - } - - public override FilterNode Visit(CompareFilter nodeIn) - { - return new CompareFilter(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/PropertyPath.cs b/src/Squidex.Infrastructure/Queries/PropertyPath.cs deleted file mode 100644 index 552d9f5c1..000000000 --- a/src/Squidex.Infrastructure/Queries/PropertyPath.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.ObjectModel; -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class PropertyPath : ReadOnlyCollection - { - private static readonly char[] Separators = { '.', '/' }; - - public PropertyPath(IList items) - : base(items) - { - if (items.Count == 0) - { - throw new ArgumentException("Path cannot be empty.", nameof(items)); - } - } - - public static implicit operator PropertyPath(string path) - { - return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries).ToList()); - } - - public static implicit operator PropertyPath(string[] path) - { - return new PropertyPath(path?.ToList()); - } - - public static implicit operator PropertyPath(List path) - { - return new PropertyPath(path); - } - - public static implicit operator PropertyPath(ImmutableList path) - { - return new PropertyPath(path); - } - - public override string ToString() - { - return string.Join(".", this); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/Query.cs b/src/Squidex.Infrastructure/Queries/Query.cs deleted file mode 100644 index 83dc03ff3..000000000 --- a/src/Squidex.Infrastructure/Queries/Query.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Infrastructure.Queries -{ - public class Query - { - public FilterNode Filter { get; set; } - - public string FullText { get; set; } - - public long Skip { get; set; } - - public long Take { get; set; } = long.MaxValue; - - public List Sort { get; set; } = new List(); - - public override string ToString() - { - var parts = new List(); - - if (Filter != null) - { - parts.Add($"Filter: {Filter}"); - } - - if (FullText != null) - { - parts.Add($"FullText: '{FullText.Replace("'", "\'")}'"); - } - - if (Skip > 0) - { - parts.Add($"Skip: {Skip}"); - } - - if (Take < long.MaxValue) - { - parts.Add($"Take: {Take}"); - } - - if (Sort.Count > 0) - { - parts.Add($"Sort: {string.Join(", ", Sort)}"); - } - - return string.Join("; ", parts); - } - } -} diff --git a/src/Squidex.Infrastructure/Queries/SortNode.cs b/src/Squidex.Infrastructure/Queries/SortNode.cs deleted file mode 100644 index fa4e47919..000000000 --- a/src/Squidex.Infrastructure/Queries/SortNode.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Queries -{ - public sealed class SortNode - { - public PropertyPath Path { get; } - - public SortOrder Order { get; set; } - - public SortNode(PropertyPath path, SortOrder order) - { - Guard.NotNull(path, nameof(path)); - Guard.Enum(order, nameof(order)); - - Path = path; - - Order = order; - } - - public override string ToString() - { - var path = string.Join(".", Path); - - return $"{path} {Order}"; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Queries/TransformVisitor.cs b/src/Squidex.Infrastructure/Queries/TransformVisitor.cs deleted file mode 100644 index d71e20403..000000000 --- a/src/Squidex.Infrastructure/Queries/TransformVisitor.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; - -namespace Squidex.Infrastructure.Queries -{ - public abstract class TransformVisitor : FilterNodeVisitor, TValue> - { - public override FilterNode Visit(CompareFilter nodeIn) - { - return nodeIn; - } - - public override FilterNode Visit(LogicalFilter nodeIn) - { - return new LogicalFilter(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); - } - - public override FilterNode Visit(NegateFilter nodeIn) - { - return new NegateFilter(nodeIn.Filter.Accept(this)); - } - } -} diff --git a/src/Squidex.Infrastructure/RefToken.cs b/src/Squidex.Infrastructure/RefToken.cs deleted file mode 100644 index 1d01ba267..000000000 --- a/src/Squidex.Infrastructure/RefToken.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure -{ - public sealed class RefToken : IEquatable - { - public string Type { get; } - - public string Identifier { get; } - - public bool IsClient - { - get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } - } - - public bool IsSubject - { - get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } - } - - public RefToken(string type, string identifier) - { - Guard.NotNullOrEmpty(type, nameof(type)); - Guard.NotNullOrEmpty(identifier, nameof(identifier)); - - Type = type.ToLowerInvariant(); - - Identifier = identifier; - } - - public override string ToString() - { - return $"{Type}:{Identifier}"; - } - - public override bool Equals(object obj) - { - return Equals(obj as RefToken); - } - - public bool Equals(RefToken other) - { - return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier))); - } - - public override int GetHashCode() - { - return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode(); - } - - public static bool TryParse(string value, out RefToken result) - { - if (value != null) - { - var idx = value.IndexOf(':'); - - if (idx > 0 && idx < value.Length - 1) - { - result = new RefToken(value.Substring(0, idx), value.Substring(idx + 1)); - - return true; - } - } - - result = null; - - return false; - } - - public static RefToken Parse(string value) - { - if (!TryParse(value, out var result)) - { - throw new ArgumentException("Ref token must have more than 2 parts divided by colon.", nameof(value)); - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs b/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs deleted file mode 100644 index 7db4f572a..000000000 --- a/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Reflection -{ - public interface IPropertyAccessor - { - object Get(object target); - - void Set(object target, object value); - } -} diff --git a/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs b/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs deleted file mode 100644 index defd58e92..000000000 --- a/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; - -namespace Squidex.Infrastructure.Reflection -{ - public sealed class PropertiesTypeAccessor - { - private static readonly ConcurrentDictionary AccessorCache = new ConcurrentDictionary(); - private readonly Dictionary accessors = new Dictionary(); - private readonly List properties = new List(); - - public IEnumerable Properties - { - get - { - return properties; - } - } - - private PropertiesTypeAccessor(Type type) - { - var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); - - foreach (var property in allProperties) - { - accessors[property.Name] = new PropertyAccessor(type, property); - - properties.Add(property); - } - } - - public static PropertiesTypeAccessor Create(Type targetType) - { - Guard.NotNull(targetType, nameof(targetType)); - - return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x)); - } - - public void SetValue(object target, string propertyName, object value) - { - Guard.NotNull(target, "target"); - - var accessor = FindAccessor(propertyName); - - accessor.Set(target, value); - } - - public object GetValue(object target, string propertyName) - { - Guard.NotNull(target, nameof(target)); - - var accessor = FindAccessor(propertyName); - - return accessor.Get(target); - } - - private IPropertyAccessor FindAccessor(string propertyName) - { - Guard.NotNullOrEmpty(propertyName, nameof(propertyName)); - - if (!accessors.TryGetValue(propertyName, out var accessor)) - { - throw new ArgumentException("Property does not exist.", nameof(propertyName)); - } - - return accessor; - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs b/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs deleted file mode 100644 index 456caccbd..000000000 --- a/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Reflection; - -namespace Squidex.Infrastructure.Reflection -{ - public sealed class PropertyAccessor : IPropertyAccessor - { - private sealed class PropertyWrapper : IPropertyAccessor - { - private readonly Func getMethod; - private readonly Action setMethod; - - public PropertyWrapper(PropertyInfo propertyInfo) - { - if (propertyInfo.CanRead) - { - getMethod = (Func)propertyInfo.GetGetMethod(true).CreateDelegate(typeof(Func)); - } - else - { - getMethod = x => throw new NotSupportedException(); - } - - if (propertyInfo.CanWrite) - { - setMethod = (Action)propertyInfo.GetSetMethod(true).CreateDelegate(typeof(Action)); - } - else - { - setMethod = (x, y) => throw new NotSupportedException(); - } - } - - public object Get(object source) - { - return getMethod((TObject)source); - } - - public void Set(object source, object value) - { - setMethod((TObject)source, (TValue)value); - } - } - - private readonly IPropertyAccessor internalAccessor; - - public PropertyAccessor(Type targetType, PropertyInfo propertyInfo) - { - Guard.NotNull(targetType, nameof(targetType)); - Guard.NotNull(propertyInfo, nameof(propertyInfo)); - - internalAccessor = (IPropertyAccessor)Activator.CreateInstance(typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType), propertyInfo); - } - - public object Get(object target) - { - Guard.NotNull(target, nameof(target)); - - return internalAccessor.Get(target); - } - - public void Set(object target, object value) - { - Guard.NotNull(target, nameof(target)); - - internalAccessor.Set(target, value); - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs b/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs deleted file mode 100644 index 7967e4da6..000000000 --- a/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure.Reflection -{ - public static class SimpleCopier - { - private struct PropertyMapper - { - private readonly IPropertyAccessor accessor; - private readonly Func converter; - - public PropertyMapper(IPropertyAccessor accessor, Func converter) - { - this.accessor = accessor; - this.converter = converter; - } - - public void MapProperty(object source, object target) - { - var value = converter(accessor.Get(source)); - - accessor.Set(target, value); - } - } - - private static class ClassCopier where T : class, new() - { - private static readonly List Mappers = new List(); - - static ClassCopier() - { - var type = typeof(T); - - foreach (var property in type.GetPublicProperties()) - { - if (!property.CanWrite || !property.CanRead) - { - continue; - } - - var accessor = new PropertyAccessor(type, property); - - if (property.PropertyType.Implements()) - { - Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x)?.Clone())); - } - else - { - Mappers.Add(new PropertyMapper(accessor, x => x)); - } - } - } - - public static T CopyThis(T source) - { - var destination = new T(); - - foreach (var mapper in Mappers) - { - mapper.MapProperty(source, destination); - } - - return destination; - } - } - - public static T Copy(this T source) where T : class, new() - { - Guard.NotNull(source, nameof(source)); - - return ClassCopier.CopyThis(source); - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs deleted file mode 100644 index 29890e85b..000000000 --- a/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs +++ /dev/null @@ -1,186 +0,0 @@ -// ========================================================================== -// 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.Globalization; -using System.Linq; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Infrastructure.Reflection -{ - public static class SimpleMapper - { - private sealed class StringConversionPropertyMapper : PropertyMapper - { - public StringConversionPropertyMapper( - IPropertyAccessor sourceAccessor, - IPropertyAccessor targetAccessor) - : base(sourceAccessor, targetAccessor) - { - } - - public override void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - SetValue(target, value?.ToString()); - } - } - - private sealed class ConversionPropertyMapper : PropertyMapper - { - private readonly Type targetType; - - public ConversionPropertyMapper( - IPropertyAccessor sourceAccessor, - IPropertyAccessor targetAccessor, - Type targetType) - : base(sourceAccessor, targetAccessor) - { - this.targetType = targetType; - } - - public override void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - if (value == null) - { - return; - } - - try - { - var converted = Convert.ChangeType(value, targetType, culture); - - SetValue(target, converted); - } - catch - { - return; - } - } - } - - private class PropertyMapper - { - private readonly IPropertyAccessor sourceAccessor; - private readonly IPropertyAccessor targetAccessor; - - public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) - { - this.sourceAccessor = sourceAccessor; - this.targetAccessor = targetAccessor; - } - - public virtual void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - SetValue(target, value); - } - - protected void SetValue(object destination, object value) - { - targetAccessor.Set(destination, value); - } - - protected object GetValue(object source) - { - return sourceAccessor.Get(source); - } - } - - private static class ClassMapper where TSource : class where TTarget : class - { - private static readonly List Mappers = new List(); - - static ClassMapper() - { - var sourceClassType = typeof(TSource); - var sourceProperties = - sourceClassType.GetPublicProperties() - .Where(x => x.CanRead).ToList(); - - var targetClassType = typeof(TTarget); - var targetProperties = - targetClassType.GetPublicProperties() - .Where(x => x.CanWrite).ToList(); - - foreach (var sourceProperty in sourceProperties) - { - var targetProperty = targetProperties.FirstOrDefault(x => x.Name == sourceProperty.Name); - - if (targetProperty == null) - { - continue; - } - - var sourceType = sourceProperty.PropertyType; - var targetType = targetProperty.PropertyType; - - if (sourceType == targetType) - { - Mappers.Add(new PropertyMapper( - new PropertyAccessor(sourceClassType, sourceProperty), - new PropertyAccessor(targetClassType, targetProperty))); - } - else if (targetType == typeof(string)) - { - Mappers.Add(new StringConversionPropertyMapper( - new PropertyAccessor(sourceClassType, sourceProperty), - new PropertyAccessor(targetClassType, targetProperty))); - } - else if (sourceType.Implements() || targetType.Implements()) - { - Mappers.Add(new ConversionPropertyMapper( - new PropertyAccessor(sourceClassType, sourceProperty), - new PropertyAccessor(targetClassType, targetProperty), - targetType)); - } - } - } - - public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) - { - foreach (var mapper in Mappers) - { - mapper.MapProperty(source, destination, culture); - } - - return destination; - } - } - - public static TTarget Map(TSource source) - where TSource : class - where TTarget : class, new() - { - return Map(source, new TTarget(), CultureInfo.CurrentCulture); - } - - public static TTarget Map(TSource source, TTarget target) - where TSource : class - where TTarget : class - { - return Map(source, target, CultureInfo.CurrentCulture); - } - - public static TTarget Map(TSource source, TTarget target, CultureInfo culture) - where TSource : class - where TTarget : class - { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(culture, nameof(culture)); - Guard.NotNull(target, nameof(target)); - - return ClassMapper.MapClass(source, target, culture); - } - } -} diff --git a/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs b/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs deleted file mode 100644 index 152be33ae..000000000 --- a/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// 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.Reflection; - -namespace Squidex.Infrastructure.Reflection -{ - public sealed class TypeNameRegistry - { - private readonly Dictionary namesByType = new Dictionary(); - private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public TypeNameRegistry(IEnumerable providers = null) - { - if (providers != null) - { - foreach (var provider in providers) - { - Map(provider); - } - } - } - - public TypeNameRegistry MapObsolete(Type type, string name) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(name, nameof(name)); - - lock (namesByType) - { - if (typesByName.TryGetValue(name, out var existingType) && existingType != type) - { - var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - - throw new ArgumentException(message, nameof(type)); - } - - typesByName[name] = type; - } - - return this; - } - - public TypeNameRegistry Map(ITypeProvider provider) - { - Guard.NotNull(provider, nameof(provider)); - - provider.Map(this); - - return this; - } - - public TypeNameRegistry Map(Type type) - { - Guard.NotNull(type, nameof(type)); - - var typeNameAttribute = type.GetCustomAttribute(); - - if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) - { - Map(type, typeNameAttribute.TypeName); - } - - return this; - } - - public TypeNameRegistry Map(Type type, string name) - { - Guard.NotNull(type, nameof(type)); - Guard.NotNull(name, nameof(name)); - - lock (namesByType) - { - if (namesByType.TryGetValue(type, out var existingName) && existingName != name) - { - var message = $"The type '{type}' is already registered with name '{namesByType[type]}'"; - - throw new ArgumentException(message, nameof(type)); - } - - namesByType[type] = name; - - if (typesByName.TryGetValue(name, out var existingType) && existingType != type) - { - var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - - throw new ArgumentException(message, nameof(type)); - } - - typesByName[name] = type; - } - - return this; - } - - public TypeNameRegistry MapUnmapped(Assembly assembly) - { - foreach (var type in assembly.GetTypes()) - { - if (!namesByType.ContainsKey(type)) - { - Map(type); - } - } - - return this; - } - - public string GetName() - { - return GetName(typeof(T)); - } - - public string GetNameOrNull() - { - return GetNameOrNull(typeof(T)); - } - - public string GetNameOrNull(Type type) - { - var result = namesByType.GetOrDefault(type); - - return result; - } - - public Type GetTypeOrNull(string name) - { - var result = typesByName.GetOrDefault(name); - - return result; - } - - public string GetName(Type type) - { - var result = namesByType.GetOrDefault(type); - - if (result == null) - { - throw new TypeNameNotFoundException($"There is no name for type '{type}"); - } - - return result; - } - - public Type GetType(string name) - { - var result = typesByName.GetOrDefault(name); - - if (result == null) - { - throw new TypeNameNotFoundException($"There is no type for name '{name}"); - } - - return result; - } - } -} diff --git a/src/Squidex.Infrastructure/RetryWindow.cs b/src/Squidex.Infrastructure/RetryWindow.cs deleted file mode 100644 index ed155d2d8..000000000 --- a/src/Squidex.Infrastructure/RetryWindow.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Squidex.Infrastructure -{ - public sealed class RetryWindow - { - private readonly Duration windowDuration; - private readonly int windowSize; - private readonly Queue retries = new Queue(); - private readonly IClock clock; - - public RetryWindow(TimeSpan windowDuration, int windowSize, IClock clock = null) - { - this.windowDuration = Duration.FromTimeSpan(windowDuration); - this.windowSize = windowSize + 1; - - this.clock = clock ?? SystemClock.Instance; - } - - public void Reset() - { - retries.Clear(); - } - - public bool CanRetryAfterFailure() - { - var now = clock.GetCurrentInstant(); - - retries.Enqueue(now); - - while (retries.Count > windowSize) - { - retries.Dequeue(); - } - - return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); - } - } -} diff --git a/src/Squidex.Infrastructure/Security/Extensions.cs b/src/Squidex.Infrastructure/Security/Extensions.cs deleted file mode 100644 index b6b5e93f5..000000000 --- a/src/Squidex.Infrastructure/Security/Extensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Security.Claims; - -namespace Squidex.Infrastructure.Security -{ - public static class Extensions - { - public static RefToken Token(this ClaimsPrincipal principal) - { - var subjectId = principal.OpenIdSubject(); - - if (!string.IsNullOrWhiteSpace(subjectId)) - { - return new RefToken(RefTokenType.Subject, subjectId); - } - - var clientId = principal.OpenIdClientId(); - - if (!string.IsNullOrWhiteSpace(clientId)) - { - return new RefToken(RefTokenType.Client, clientId); - } - - return null; - } - - public static string OpenIdSubject(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Subject)?.Value; - } - - public static string OpenIdClientId(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; - } - - public static string UserOrClientId(this ClaimsPrincipal principal) - { - return principal.OpenIdSubject() ?? principal.OpenIdClientId(); - } - - public static string OpenIdPreferredUserName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; - } - - public static string OpenIdName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Name)?.Value; - } - - public static string OpenIdNickName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.NickName)?.Value; - } - - public static string OpenIdEmail(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; - } - - public static bool IsInClient(this ClaimsPrincipal principal, string client) - { - return principal.Claims.Any(x => x.Type == OpenIdClaims.ClientId && string.Equals(x.Value, client, StringComparison.OrdinalIgnoreCase)); - } - } -} diff --git a/src/Squidex.Infrastructure/Security/Permission.Part.cs b/src/Squidex.Infrastructure/Security/Permission.Part.cs deleted file mode 100644 index 47ecf2e5a..000000000 --- a/src/Squidex.Infrastructure/Security/Permission.Part.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; - -namespace Squidex.Infrastructure.Security -{ - public sealed partial class Permission - { - internal struct Part - { - private static readonly char[] AlternativeSeparators = { '|' }; - private static readonly char[] MainSeparators = { '.' }; - - public readonly string[] Alternatives; - - public readonly bool Exclusion; - - public Part(string[] alternatives, bool exclusion) - { - Alternatives = alternatives; - - Exclusion = exclusion; - } - - public static Part[] ParsePath(string path) - { - var parts = path.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries); - - var result = new Part[parts.Length]; - - for (var i = 0; i < result.Length; i++) - { - result[i] = Parse(parts[i]); - } - - return result; - } - - public static Part Parse(string part) - { - var isExclusion = false; - - if (part.StartsWith(Exclude, StringComparison.OrdinalIgnoreCase)) - { - isExclusion = true; - - part = part.Substring(1); - } - - string[] alternatives = null; - - if (part != Any) - { - alternatives = part.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries); - } - - return new Part(alternatives, isExclusion); - } - - public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) - { - if (lhs.Alternatives == null) - { - return true; - } - - if (allowNull && rhs.Alternatives == null) - { - return true; - } - - var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); - - return rhs.Alternatives != null && lhs.Alternatives.Intersect(rhs.Alternatives).Any() == shouldIntersect; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Security/Permission.cs b/src/Squidex.Infrastructure/Security/Permission.cs deleted file mode 100644 index 19c6dc089..000000000 --- a/src/Squidex.Infrastructure/Security/Permission.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.Security -{ - public sealed partial class Permission : IComparable, IEquatable - { - public const string Any = "*"; - public const string Exclude = "^"; - - private readonly string id; - private Part[] path; - - public string Id - { - get { return id; } - } - - private Part[] Path - { - get { return path ?? (path = Part.ParsePath(id)); } - } - - public Permission(string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - this.id = id; - } - - public bool Allows(Permission permission) - { - if (permission == null) - { - return false; - } - - return Covers(Path, permission.Path); - } - - public bool Includes(Permission permission) - { - if (permission == null) - { - return false; - } - - return PartialCovers(Path, permission.Path); - } - - private static bool Covers(Part[] given, Part[] requested) - { - if (given.Length > requested.Length) - { - return false; - } - - for (var i = 0; i < given.Length; i++) - { - if (!Part.Intersects(ref given[i], ref requested[i], false)) - { - return false; - } - } - - return true; - } - - private static bool PartialCovers(Part[] given, Part[] requested) - { - for (var i = 0; i < Math.Min(given.Length, requested.Length); i++) - { - if (!Part.Intersects(ref given[i], ref requested[i], true)) - { - return false; - } - } - - return true; - } - - public bool StartsWith(string test) - { - return id.StartsWith(test, StringComparison.OrdinalIgnoreCase); - } - - public override bool Equals(object obj) - { - return Equals(obj as Permission); - } - - public bool Equals(Permission other) - { - return other != null && string.Equals(id, other.id, StringComparison.OrdinalIgnoreCase); - } - - public override int GetHashCode() - { - return id.GetHashCode(); - } - - public override string ToString() - { - return id; - } - - public int CompareTo(Permission other) - { - return other == null ? -1 : string.Compare(id, other.id, StringComparison.Ordinal); - } - } -} diff --git a/src/Squidex.Infrastructure/Security/PermissionSet.cs b/src/Squidex.Infrastructure/Security/PermissionSet.cs deleted file mode 100644 index 08f702b19..000000000 --- a/src/Squidex.Infrastructure/Security/PermissionSet.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure.Security -{ - public sealed class PermissionSet : IReadOnlyCollection - { - public static readonly PermissionSet Empty = new PermissionSet(Array.Empty()); - - private readonly List permissions; - private readonly Lazy display; - - public int Count - { - get { return permissions.Count; } - } - - public PermissionSet(params Permission[] permissions) - : this((IEnumerable)permissions) - { - } - - public PermissionSet(params string[] permissions) - : this(permissions?.Select(x => new Permission(x))) - { - } - - public PermissionSet(IEnumerable permissions) - : this(permissions?.Select(x => new Permission(x))) - { - } - - public PermissionSet(IEnumerable permissions) - { - Guard.NotNull(permissions, nameof(permissions)); - - this.permissions = permissions.ToList(); - - display = new Lazy(() => string.Join(";", this.permissions)); - } - - public bool Allows(Permission other) - { - if (other == null) - { - return false; - } - - return permissions.Any(x => x.Allows(other)); - } - - public bool Includes(Permission other) - { - if (other == null) - { - return false; - } - - return permissions.Any(x => x.Includes(other)); - } - - public override string ToString() - { - return display.Value; - } - - public IEnumerable ToIds() - { - return permissions.Select(x => x.Id); - } - - public IEnumerator GetEnumerator() - { - return permissions.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return permissions.GetEnumerator(); - } - } -} diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj deleted file mode 100644 index aa339402d..000000000 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs deleted file mode 100644 index cc7be1f10..000000000 --- a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs +++ /dev/null @@ -1,45 +0,0 @@ - // ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.States -{ - public sealed class DefaultStreamNameResolver : IStreamNameResolver - { - private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; - - public string GetStreamName(Type aggregateType, string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - Guard.NotNull(aggregateType, nameof(aggregateType)); - - return $"{aggregateType.TypeName(true, Suffixes)}-{id}"; - } - - public string WithNewId(string streamName, Func idGenerator) - { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(idGenerator, nameof(idGenerator)); - - var positionOfDash = streamName.IndexOf('-'); - - if (positionOfDash >= 0) - { - var newId = idGenerator(streamName.Substring(positionOfDash + 1)); - - if (!string.IsNullOrWhiteSpace(newId)) - { - streamName = $"{streamName.Substring(0, positionOfDash)}-{newId}"; - } - } - - return streamName; - } - } -} diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs deleted file mode 100644 index 390e555de..000000000 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.States -{ - public delegate void HandleEvent(Envelope @event); - - public delegate void HandleSnapshot(T state); - - public interface IStore - { - IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent applyEvent); - - IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot applySnapshot); - - IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot applySnapshot, HandleEvent applyEvent); - - ISnapshotStore GetSnapshotStore(); - } -} diff --git a/src/Squidex.Infrastructure/States/IStreamNameResolver.cs b/src/Squidex.Infrastructure/States/IStreamNameResolver.cs deleted file mode 100644 index 02b15f2fb..000000000 --- a/src/Squidex.Infrastructure/States/IStreamNameResolver.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.States -{ - public interface IStreamNameResolver - { - string GetStreamName(Type aggregateType, string id); - - string WithNewId(string streamName, Func idGenerator); - } -} diff --git a/src/Squidex.Infrastructure/States/InconsistentStateException.cs b/src/Squidex.Infrastructure/States/InconsistentStateException.cs deleted file mode 100644 index ccdd14a08..000000000 --- a/src/Squidex.Infrastructure/States/InconsistentStateException.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Runtime.Serialization; - -namespace Squidex.Infrastructure.States -{ - [Serializable] - public class InconsistentStateException : Exception - { - private readonly long currentVersion; - private readonly long expectedVersion; - - public long CurrentVersion - { - get { return currentVersion; } - } - - public long ExpectedVersion - { - get { return expectedVersion; } - } - - public InconsistentStateException(long currentVersion, long expectedVersion, Exception inner = null) - : base(FormatMessage(currentVersion, expectedVersion), inner) - { - this.currentVersion = currentVersion; - - this.expectedVersion = expectedVersion; - } - - protected InconsistentStateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - currentVersion = info.GetInt64(nameof(currentVersion)); - - expectedVersion = info.GetInt64(nameof(expectedVersion)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(currentVersion), currentVersion); - info.AddValue(nameof(expectedVersion), expectedVersion); - - base.GetObjectData(info, context); - } - - private static string FormatMessage(long currentVersion, long expectedVersion) - { - return $"Requested version {expectedVersion}, but found {currentVersion}."; - } - } -} diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs deleted file mode 100644 index df65d0ddc..000000000 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.States -{ - internal sealed class Persistence : Persistence, IPersistence - { - public Persistence(TKey ownerKey, Type ownerType, - IEventStore eventStore, - IEventEnricher eventEnricher, - IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, - IStreamNameResolver streamNameResolver, - HandleEvent applyEvent) - : base(ownerKey, ownerType, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) - { - } - } -} diff --git a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs deleted file mode 100644 index c59303c17..000000000 --- a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs +++ /dev/null @@ -1,241 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement - -namespace Squidex.Infrastructure.States -{ - internal class Persistence : IPersistence - { - private readonly TKey ownerKey; - private readonly Type ownerType; - private readonly ISnapshotStore snapshotStore; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventEnricher eventEnricher; - private readonly IEventDataFormatter eventDataFormatter; - private readonly PersistenceMode persistenceMode; - private readonly HandleSnapshot applyState; - private readonly HandleEvent applyEvent; - private long versionSnapshot = EtagVersion.Empty; - private long versionEvents = EtagVersion.Empty; - private long version; - - public long Version - { - get { return version; } - } - - public Persistence(TKey ownerKey, Type ownerType, - IEventStore eventStore, - IEventEnricher eventEnricher, - IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, - IStreamNameResolver streamNameResolver, - PersistenceMode persistenceMode, - HandleSnapshot applyState, - HandleEvent applyEvent) - { - this.ownerKey = ownerKey; - this.ownerType = ownerType; - this.applyState = applyState; - this.applyEvent = applyEvent; - this.eventStore = eventStore; - this.eventEnricher = eventEnricher; - this.eventDataFormatter = eventDataFormatter; - this.persistenceMode = persistenceMode; - this.snapshotStore = snapshotStore; - this.streamNameResolver = streamNameResolver; - } - - public async Task ReadAsync(long expectedVersion = EtagVersion.Any) - { - versionSnapshot = EtagVersion.Empty; - versionEvents = EtagVersion.Empty; - - await ReadSnapshotAsync(); - await ReadEventsAsync(); - - UpdateVersion(); - - if (expectedVersion > EtagVersion.Any && expectedVersion != version) - { - if (version == EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(ownerKey.ToString(), ownerType); - } - else - { - throw new InconsistentStateException(version, expectedVersion); - } - } - } - - private async Task ReadSnapshotAsync() - { - if (UseSnapshots()) - { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); - - if (position < EtagVersion.Empty) - { - position = EtagVersion.Empty; - } - - versionSnapshot = position; - versionEvents = position; - - if (applyState != null && position >= 0) - { - applyState(state); - } - } - } - - private async Task ReadEventsAsync() - { - if (UseEventSourcing()) - { - var events = await eventStore.QueryAsync(GetStreamName(), versionEvents + 1); - - foreach (var @event in events) - { - versionEvents++; - - if (@event.EventStreamNumber != versionEvents) - { - throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); - } - - var parsedEvent = ParseKnownEvent(@event); - - if (applyEvent != null && parsedEvent != null) - { - applyEvent(parsedEvent); - } - } - } - } - - public async Task WriteSnapshotAsync(TSnapshot state) - { - var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - - if (newVersion != versionSnapshot) - { - await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); - - versionSnapshot = newVersion; - } - - UpdateVersion(); - } - - public async Task WriteEventsAsync(IEnumerable> events) - { - Guard.NotNull(events, nameof(events)); - - var eventArray = events.ToArray(); - - if (eventArray.Length > 0) - { - var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; - - var commitId = Guid.NewGuid(); - - foreach (var @event in eventArray) - { - eventEnricher.Enrich(@event, ownerKey); - } - - var eventStream = GetStreamName(); - var eventData = GetEventData(eventArray, commitId); - - try - { - await eventStore.AppendAsync(commitId, eventStream, expectedVersion, eventData); - } - catch (WrongEventVersionException ex) - { - throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); - } - - versionEvents += eventArray.Length; - } - - UpdateVersion(); - } - - public async Task DeleteAsync() - { - if (UseEventSourcing()) - { - await eventStore.DeleteStreamAsync(GetStreamName()); - } - - if (UseSnapshots()) - { - await snapshotStore.RemoveAsync(ownerKey); - } - } - - private EventData[] GetEventData(Envelope[] events, Guid commitId) - { - return events.Map(x => eventDataFormatter.ToEventData(x, commitId, true)); - } - - private string GetStreamName() - { - return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()); - } - - private bool UseSnapshots() - { - return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private bool UseEventSourcing() - { - return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private Envelope ParseKnownEvent(StoredEvent storedEvent) - { - try - { - return eventDataFormatter.Parse(storedEvent.Data); - } - catch (TypeNameNotFoundException) - { - return null; - } - } - - private void UpdateVersion() - { - if (persistenceMode == PersistenceMode.Snapshots) - { - version = versionSnapshot; - } - else if (persistenceMode == PersistenceMode.EventSourcing) - { - version = versionEvents; - } - else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) - { - version = Math.Max(versionEvents, versionSnapshot); - } - } - } -} diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs deleted file mode 100644 index 2131258ec..000000000 --- a/src/Squidex.Infrastructure/States/Store.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.States -{ - public sealed class Store : IStore - { - private readonly IServiceProvider services; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventEnricher eventEnricher; - private readonly IEventDataFormatter eventDataFormatter; - - public Store( - IEventStore eventStore, - IEventEnricher eventEnricher, - IEventDataFormatter eventDataFormatter, - IServiceProvider services, - IStreamNameResolver streamNameResolver) - { - this.eventStore = eventStore; - this.eventEnricher = eventEnricher; - this.eventDataFormatter = eventDataFormatter; - this.services = services; - this.streamNameResolver = streamNameResolver; - } - - public IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent applyEvent) - { - return CreatePersistence(owner, key, applyEvent); - } - - public IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot applySnapshot) - { - return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); - } - - public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot applySnapshot, HandleEvent applyEvent) - { - return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); - } - - private IPersistence CreatePersistence(Type owner, TKey key, HandleEvent applyEvent) - { - Guard.NotNull(key, nameof(key)); - - var snapshotStore = GetSnapshotStore(); - - return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); - } - - private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, HandleSnapshot applySnapshot, HandleEvent applyEvent) - { - Guard.NotNull(key, nameof(key)); - - var snapshotStore = GetSnapshotStore(); - - return new Persistence(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); - } - - public ISnapshotStore GetSnapshotStore() - { - return (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - } - } -} diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs deleted file mode 100644 index af47c2d17..000000000 --- a/src/Squidex.Infrastructure/StringExtensions.cs +++ /dev/null @@ -1,801 +0,0 @@ -// ========================================================================== -// 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 System.Text; -using System.Text.RegularExpressions; - -namespace Squidex.Infrastructure -{ - public static class StringExtensions - { - private const char NullChar = (char)0; - - private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); - private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); - private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); - - private static readonly Dictionary LowerCaseDiacritics; - private static readonly Dictionary Diacritics = new Dictionary - { - ['$'] = "dollar", - ['%'] = "percent", - ['&'] = "and", - ['<'] = "less", - ['>'] = "greater", - ['|'] = "or", - ['¢'] = "cent", - ['£'] = "pound", - ['¤'] = "currency", - ['¥'] = "yen", - ['©'] = "(c)", - ['ª'] = "a", - ['®'] = "(r)", - ['º'] = "o", - ['À'] = "A", - ['Á'] = "A", - ['Â'] = "A", - ['Ã'] = "A", - ['Ä'] = "AE", - ['Å'] = "A", - ['Æ'] = "AE", - ['Ç'] = "C", - ['Ə'] = "E", - ['È'] = "E", - ['É'] = "E", - ['Ê'] = "E", - ['Ë'] = "E", - ['Ì'] = "I", - ['Í'] = "I", - ['Î'] = "I", - ['Ï'] = "I", - ['Ð'] = "D", - ['Ñ'] = "N", - ['Ò'] = "O", - ['Ó'] = "O", - ['Ô'] = "O", - ['Õ'] = "O", - ['Ö'] = "OE", - ['Ø'] = "O", - ['Ù'] = "U", - ['Ú'] = "U", - ['Û'] = "U", - ['Ü'] = "UE", - ['Ý'] = "Y", - ['Þ'] = "TH", - ['ß'] = "ss", - ['à'] = "a", - ['á'] = "a", - ['â'] = "a", - ['ã'] = "a", - ['ä'] = "ae", - ['å'] = "a", - ['æ'] = "ae", - ['ç'] = "c", - ['ə'] = "e", - ['è'] = "e", - ['é'] = "e", - ['ê'] = "e", - ['ë'] = "e", - ['ì'] = "i", - ['í'] = "i", - ['î'] = "i", - ['ï'] = "i", - ['ð'] = "d", - ['ñ'] = "n", - ['ò'] = "o", - ['ó'] = "o", - ['ô'] = "o", - ['õ'] = "o", - ['ö'] = "oe", - ['ø'] = "o", - ['ù'] = "u", - ['ú'] = "u", - ['û'] = "u", - ['ü'] = "ue", - ['ý'] = "y", - ['þ'] = "th", - ['ÿ'] = "y", - ['Ā'] = "A", - ['ā'] = "a", - ['Ă'] = "A", - ['ă'] = "a", - ['Ą'] = "A", - ['ą'] = "a", - ['Ć'] = "C", - ['ć'] = "c", - ['Č'] = "C", - ['č'] = "c", - ['Ď'] = "D", - ['ď'] = "d", - ['Đ'] = "DJ", - ['đ'] = "dj", - ['Ē'] = "E", - ['ē'] = "e", - ['Ė'] = "E", - ['ė'] = "e", - ['Ę'] = "e", - ['ę'] = "e", - ['Ě'] = "E", - ['ě'] = "e", - ['Ğ'] = "G", - ['ğ'] = "g", - ['Ģ'] = "G", - ['ģ'] = "g", - ['Ĩ'] = "I", - ['ĩ'] = "i", - ['Ī'] = "i", - ['ī'] = "i", - ['Į'] = "I", - ['į'] = "i", - ['İ'] = "I", - ['ı'] = "i", - ['Ķ'] = "k", - ['ķ'] = "k", - ['Ļ'] = "L", - ['ļ'] = "l", - ['Ľ'] = "L", - ['ľ'] = "l", - ['Ł'] = "L", - ['ł'] = "l", - ['Ń'] = "N", - ['ń'] = "n", - ['Ņ'] = "N", - ['ņ'] = "n", - ['Ň'] = "N", - ['ň'] = "n", - ['Ő'] = "O", - ['ő'] = "o", - ['Œ'] = "OE", - ['œ'] = "oe", - ['Ŕ'] = "R", - ['ŕ'] = "r", - ['Ř'] = "R", - ['ř'] = "r", - ['Ś'] = "S", - ['ś'] = "s", - ['Ş'] = "S", - ['ş'] = "s", - ['Š'] = "S", - ['š'] = "s", - ['Ţ'] = "T", - ['ţ'] = "t", - ['Ť'] = "T", - ['ť'] = "t", - ['Ũ'] = "U", - ['ũ'] = "u", - ['Ū'] = "u", - ['ū'] = "u", - ['Ů'] = "U", - ['ů'] = "u", - ['Ű'] = "U", - ['ű'] = "u", - ['Ų'] = "U", - ['ų'] = "u", - ['Ź'] = "Z", - ['ź'] = "z", - ['Ż'] = "Z", - ['ż'] = "z", - ['Ž'] = "Z", - ['ž'] = "z", - ['ƒ'] = "f", - ['Ơ'] = "O", - ['ơ'] = "o", - ['Ư'] = "U", - ['ư'] = "u", - ['Lj'] = "LJ", - ['lj'] = "lj", - ['Nj'] = "NJ", - ['nj'] = "nj", - ['Ș'] = "S", - ['ș'] = "s", - ['Ț'] = "T", - ['ț'] = "t", - ['˚'] = "o", - ['Ά'] = "A", - ['Έ'] = "E", - ['Ή'] = "H", - ['Ί'] = "I", - ['Ό'] = "O", - ['Ύ'] = "Y", - ['Ώ'] = "W", - ['ΐ'] = "i", - ['Α'] = "A", - ['Β'] = "B", - ['Γ'] = "G", - ['Δ'] = "D", - ['Ε'] = "E", - ['Ζ'] = "Z", - ['Η'] = "H", - ['Θ'] = "8", - ['Ι'] = "I", - ['Κ'] = "K", - ['Λ'] = "L", - ['Μ'] = "M", - ['Ν'] = "N", - ['Ξ'] = "3", - ['Ο'] = "O", - ['Π'] = "P", - ['Ρ'] = "R", - ['Σ'] = "S", - ['Τ'] = "T", - ['Υ'] = "Y", - ['Φ'] = "F", - ['Χ'] = "X", - ['Ψ'] = "PS", - ['Ω'] = "W", - ['Ϊ'] = "I", - ['Ϋ'] = "Y", - ['ά'] = "a", - ['έ'] = "e", - ['ή'] = "h", - ['ί'] = "i", - ['ΰ'] = "y", - ['α'] = "a", - ['β'] = "b", - ['γ'] = "g", - ['δ'] = "d", - ['ε'] = "e", - ['ζ'] = "z", - ['η'] = "h", - ['θ'] = "8", - ['ι'] = "i", - ['κ'] = "k", - ['λ'] = "l", - ['μ'] = "m", - ['ν'] = "n", - ['ξ'] = "3", - ['ο'] = "o", - ['π'] = "p", - ['ρ'] = "r", - ['ς'] = "s", - ['σ'] = "s", - ['τ'] = "t", - ['υ'] = "y", - ['φ'] = "f", - ['χ'] = "x", - ['ψ'] = "ps", - ['ω'] = "w", - ['ϊ'] = "i", - ['ϋ'] = "y", - ['ό'] = "o", - ['ύ'] = "y", - ['ώ'] = "w", - ['Ё'] = "Yo", - ['Ђ'] = "DJ", - ['Є'] = "Ye", - ['І'] = "I", - ['Ї'] = "Yi", - ['Ј'] = "J", - ['Љ'] = "LJ", - ['Њ'] = "NJ", - ['Ћ'] = "C", - ['Џ'] = "DZ", - ['А'] = "A", - ['Б'] = "B", - ['В'] = "V", - ['Г'] = "G", - ['Д'] = "D", - ['Е'] = "E", - ['Ж'] = "Zh", - ['З'] = "Z", - ['И'] = "I", - ['Й'] = "J", - ['К'] = "K", - ['Л'] = "L", - ['М'] = "M", - ['Н'] = "N", - ['О'] = "O", - ['П'] = "P", - ['Р'] = "R", - ['С'] = "S", - ['Т'] = "T", - ['У'] = "U", - ['Ф'] = "F", - ['Х'] = "H", - ['Ц'] = "C", - ['Ч'] = "Ch", - ['Ш'] = "Sh", - ['Щ'] = "Sh", - ['Ъ'] = "U", - ['Ы'] = "Y", - ['Ь'] = "b", - ['Э'] = "E", - ['Ю'] = "Yu", - ['Я'] = "Ya", - ['а'] = "a", - ['б'] = "b", - ['в'] = "v", - ['г'] = "g", - ['д'] = "d", - ['е'] = "e", - ['ж'] = "zh", - ['з'] = "z", - ['и'] = "i", - ['й'] = "j", - ['к'] = "k", - ['л'] = "l", - ['м'] = "m", - ['н'] = "n", - ['о'] = "o", - ['п'] = "p", - ['р'] = "r", - ['с'] = "s", - ['т'] = "t", - ['у'] = "u", - ['ф'] = "f", - ['х'] = "h", - ['ц'] = "c", - ['ч'] = "ch", - ['ш'] = "sh", - ['щ'] = "sh", - ['ъ'] = "u", - ['ы'] = "y", - ['ь'] = "s", - ['э'] = "e", - ['ю'] = "yu", - ['я'] = "ya", - ['ё'] = "yo", - ['ђ'] = "dj", - ['є'] = "ye", - ['і'] = "i", - ['ї'] = "yi", - ['ј'] = "j", - ['љ'] = "lj", - ['њ'] = "nj", - ['ћ'] = "c", - ['џ'] = "dz", - ['Ґ'] = "G", - ['ґ'] = "g", - ['฿'] = "baht", - ['ა'] = "a", - ['ბ'] = "b", - ['გ'] = "g", - ['დ'] = "d", - ['ე'] = "e", - ['ვ'] = "v", - ['ზ'] = "z", - ['თ'] = "t", - ['ი'] = "i", - ['კ'] = "k", - ['ლ'] = "l", - ['მ'] = "m", - ['ნ'] = "n", - ['ო'] = "o", - ['პ'] = "p", - ['ჟ'] = "zh", - ['რ'] = "r", - ['ს'] = "s", - ['ტ'] = "t", - ['უ'] = "u", - ['ფ'] = "f", - ['ქ'] = "k", - ['ღ'] = "gh", - ['ყ'] = "q", - ['შ'] = "sh", - ['ჩ'] = "ch", - ['ც'] = "ts", - ['ძ'] = "dz", - ['წ'] = "ts", - ['ჭ'] = "ch", - ['ხ'] = "kh", - ['ჯ'] = "j", - ['ჰ'] = "h", - ['ẞ'] = "SS", - ['Ạ'] = "A", - ['ạ'] = "a", - ['Ả'] = "A", - ['ả'] = "a", - ['Ấ'] = "A", - ['ấ'] = "a", - ['Ầ'] = "A", - ['ầ'] = "a", - ['Ẩ'] = "A", - ['ẩ'] = "a", - ['Ẫ'] = "A", - ['ẫ'] = "a", - ['Ậ'] = "A", - ['ậ'] = "a", - ['Ắ'] = "A", - ['ắ'] = "a", - ['Ằ'] = "A", - ['ằ'] = "a", - ['Ẳ'] = "A", - ['ẳ'] = "a", - ['Ẵ'] = "A", - ['ẵ'] = "a", - ['Ặ'] = "A", - ['ặ'] = "a", - ['Ẹ'] = "E", - ['ẹ'] = "e", - ['Ẻ'] = "E", - ['ẻ'] = "e", - ['Ẽ'] = "E", - ['ẽ'] = "e", - ['Ế'] = "E", - ['ế'] = "e", - ['Ề'] = "E", - ['ề'] = "e", - ['Ể'] = "E", - ['ể'] = "e", - ['Ễ'] = "E", - ['ễ'] = "e", - ['Ệ'] = "E", - ['ệ'] = "e", - ['Ỉ'] = "I", - ['ỉ'] = "i", - ['Ị'] = "I", - ['ị'] = "i", - ['Ọ'] = "O", - ['ọ'] = "o", - ['Ỏ'] = "O", - ['ỏ'] = "o", - ['Ố'] = "O", - ['ố'] = "o", - ['Ồ'] = "O", - ['ồ'] = "o", - ['Ổ'] = "O", - ['ổ'] = "o", - ['Ỗ'] = "O", - ['ỗ'] = "o", - ['Ộ'] = "O", - ['ộ'] = "o", - ['Ớ'] = "O", - ['ớ'] = "o", - ['Ờ'] = "O", - ['ờ'] = "o", - ['Ở'] = "O", - ['ở'] = "o", - ['Ỡ'] = "O", - ['ỡ'] = "o", - ['Ợ'] = "O", - ['ợ'] = "o", - ['Ụ'] = "U", - ['ụ'] = "u", - ['Ủ'] = "U", - ['ủ'] = "u", - ['Ứ'] = "U", - ['ứ'] = "u", - ['Ừ'] = "U", - ['ừ'] = "u", - ['Ử'] = "U", - ['ử'] = "u", - ['Ữ'] = "U", - ['ữ'] = "u", - ['Ự'] = "U", - ['ự'] = "u", - ['Ỳ'] = "Y", - ['ỳ'] = "y", - ['Ỵ'] = "Y", - ['ỵ'] = "y", - ['Ỷ'] = "Y", - ['ỷ'] = "y", - ['Ỹ'] = "Y", - ['ỹ'] = "y", - ['‘'] = "\'", - ['’'] = "\'", - ['“'] = "\\\"", - ['”'] = "\\\"", - ['†'] = "+", - ['•'] = "*", - ['…'] = "...", - ['₠'] = "ecu", - ['₢'] = "cruzeiro", - ['₣'] = "french franc", - ['₤'] = "lira", - ['₥'] = "mill", - ['₦'] = "naira", - ['₧'] = "peseta", - ['₨'] = "rupee", - ['₩'] = "won", - ['₪'] = "new shequel", - ['₫'] = "dong", - ['€'] = "euro", - ['₭'] = "kip", - ['₮'] = "tugrik", - ['₯'] = "drachma", - ['₰'] = "penny", - ['₱'] = "peso", - ['₲'] = "guarani", - ['₳'] = "austral", - ['₴'] = "hryvnia", - ['₵'] = "cedi", - ['₹'] = "indian rupee", - ['₽'] = "russian ruble", - ['₿'] = "bitcoin", - ['℠'] = "sm", - ['™'] = "tm", - ['∂'] = "d", - ['∆'] = "delta", - ['∑'] = "sum", - ['∞'] = "infinity", - ['♥'] = "love", - ['元'] = "yuan", - ['円'] = "yen", - ['﷼'] = "rial" - }; - - static StringExtensions() - { - LowerCaseDiacritics = Diacritics.ToDictionary(x => x.Key, x => x.Value.ToLowerInvariant()); - } - - public static bool IsSlug(this string value) - { - return value != null && SlugRegex.IsMatch(value); - } - - public static bool IsEmail(this string value) - { - return value != null && EmailRegex.IsMatch(value); - } - - public static bool IsPropertyName(this string value) - { - return value != null && PropertyNameRegex.IsMatch(value); - } - - public static string WithFallback(this string value, string fallback) - { - return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; - } - - public static string ToPascalCase(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return string.Empty; - } - - var sb = new StringBuilder(value.Length); - - var last = NullChar; - var length = 0; - - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - - if (c == '-' || c == '_' || c == ' ') - { - if (last != NullChar) - { - sb.Append(char.ToUpperInvariant(last)); - } - - last = NullChar; - length = 0; - } - else - { - if (length > 1) - { - sb.Append(c); - } - else if (length == 0) - { - last = c; - } - else - { - sb.Append(char.ToUpperInvariant(last)); - sb.Append(c); - - last = NullChar; - } - - length++; - } - } - - if (last != NullChar) - { - sb.Append(char.ToUpperInvariant(last)); - } - - return sb.ToString(); - } - - public static string ToKebabCase(this string value) - { - if (value.Length == 0) - { - return string.Empty; - } - - var sb = new StringBuilder(value.Length); - - var length = 0; - - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - - if (c == '-' || c == '_' || c == ' ') - { - length = 0; - } - else - { - if (length > 0) - { - sb.Append(char.ToLowerInvariant(c)); - } - else - { - if (sb.Length > 0) - { - sb.Append('-'); - } - - sb.Append(char.ToLowerInvariant(c)); - } - - length++; - } - } - - return sb.ToString(); - } - - public static string ToCamelCase(this string value) - { - if (value.Length == 0) - { - return string.Empty; - } - - var sb = new StringBuilder(value.Length); - - var last = NullChar; - var length = 0; - - for (var i = 0; i < value.Length; i++) - { - var c = value[i]; - - if (c == '-' || c == '_' || c == ' ') - { - if (last != NullChar) - { - if (sb.Length > 0) - { - sb.Append(char.ToUpperInvariant(last)); - } - else - { - sb.Append(char.ToLowerInvariant(last)); - } - } - - last = NullChar; - length = 0; - } - else - { - if (length > 1) - { - sb.Append(c); - } - else if (length == 0) - { - last = c; - } - else - { - if (sb.Length > 0) - { - sb.Append(char.ToUpperInvariant(last)); - } - else - { - sb.Append(char.ToLowerInvariant(last)); - } - - sb.Append(c); - - last = NullChar; - } - - length++; - } - } - - if (last != NullChar) - { - if (sb.Length > 0) - { - sb.Append(char.ToUpperInvariant(last)); - } - else - { - sb.Append(char.ToLowerInvariant(last)); - } - } - - return sb.ToString(); - } - - public static string Slugify(this string value, ISet preserveHash = null, bool singleCharDiactric = false, char separator = '-') - { - var result = new StringBuilder(value.Length); - - var lastChar = (char)0; - - for (var i = 0; i < value.Length; i++) - { - var character = value[i]; - - if (preserveHash?.Contains(character) == true) - { - result.Append(character); - } - else if (char.IsLetter(character) || char.IsNumber(character)) - { - lastChar = character; - - var lower = char.ToLowerInvariant(character); - - if (LowerCaseDiacritics.TryGetValue(character, out var replacement)) - { - if (singleCharDiactric && replacement.Length == 2) - { - result.Append(replacement[0]); - } - else - { - result.Append(replacement); - } - } - else - { - result.Append(lower); - } - } - else if ((i < value.Length - 1) && (i > 0 && lastChar != separator)) - { - lastChar = separator; - - result.Append(separator); - } - } - - return result.ToString().Trim(separator); - } - - public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) - { - Guard.NotNull(path, nameof(path)); - - var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; - - if (trailingSlash && - url.IndexOf("#", StringComparison.OrdinalIgnoreCase) < 0 && - url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 && - url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0) - { - url += "/"; - } - - return url; - } - - public static string JoinNonEmpty(string separator, params string[] parts) - { - Guard.NotNull(separator, nameof(separator)); - - if (parts == null || parts.Length == 0) - { - return string.Empty; - } - - return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs b/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs deleted file mode 100644 index a9f2b8cac..000000000 --- a/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class AsyncLocalCleaner : IDisposable - { - private readonly AsyncLocal asyncLocal; - - public AsyncLocalCleaner(AsyncLocal asyncLocal) - { - Guard.NotNull(asyncLocal, nameof(asyncLocal)); - - this.asyncLocal = asyncLocal; - } - - public void Dispose() - { - asyncLocal.Value = default; - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs deleted file mode 100644 index 60e11e804..000000000 --- a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; - -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class AsyncLock - { - private readonly SemaphoreSlim semaphore; - - public AsyncLock() - { - semaphore = new SemaphoreSlim(1); - } - - public Task LockAsync() - { - var wait = semaphore.WaitAsync(); - - if (wait.IsCompleted) - { - return Task.FromResult((IDisposable)new LockReleaser(this)); - } - else - { - return wait.ContinueWith(x => (IDisposable)new LockReleaser(this), - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - } - - private class LockReleaser : IDisposable - { - private AsyncLock target; - - internal LockReleaser(AsyncLock target) - { - this.target = target; - } - - public void Dispose() - { - var current = target; - - if (current == null) - { - return; - } - - target = null; - - try - { - current.semaphore.Release(); - } - catch - { - // just ignore the Exception - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs b/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs deleted file mode 100644 index 6c8e99ba2..000000000 --- a/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class AsyncLockPool - { - private readonly AsyncLock[] locks; - - public AsyncLockPool(int poolSize) - { - Guard.GreaterThan(poolSize, 0, nameof(poolSize)); - - locks = new AsyncLock[poolSize]; - - for (var i = 0; i < poolSize; i++) - { - locks[i] = new AsyncLock(); - } - } - - public Task LockAsync(object target) - { - Guard.NotNull(target, nameof(target)); - - return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync(); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs deleted file mode 100644 index 2f61604c8..000000000 --- a/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.Tasks -{ - public class PartitionedActionBlock : ITargetBlock - { - private readonly ITargetBlock distributor; - private readonly ActionBlock[] workers; - - public Task Completion - { - get { return Task.WhenAll(workers.Select(x => x.Completion)); } - } - - public PartitionedActionBlock(Action action, Func partitioner) - : this (action?.ToAsync(), partitioner, new ExecutionDataflowBlockOptions()) - { - } - - public PartitionedActionBlock(Func action, Func partitioner) - : this(action, partitioner, new ExecutionDataflowBlockOptions()) - { - } - - public PartitionedActionBlock(Action action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - : this(action?.ToAsync(), partitioner, dataflowBlockOptions) - { - } - - public PartitionedActionBlock(Func action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - { - Guard.NotNull(action, nameof(action)); - Guard.NotNull(partitioner, nameof(partitioner)); - Guard.NotNull(dataflowBlockOptions, nameof(dataflowBlockOptions)); - Guard.GreaterThan(dataflowBlockOptions.MaxDegreeOfParallelism, 1, nameof(dataflowBlockOptions.MaxDegreeOfParallelism)); - - workers = new ActionBlock[dataflowBlockOptions.MaxDegreeOfParallelism]; - - for (var i = 0; i < dataflowBlockOptions.MaxDegreeOfParallelism; i++) - { - var workerOption = SimpleMapper.Map(dataflowBlockOptions, new ExecutionDataflowBlockOptions()); - - workerOption.MaxDegreeOfParallelism = 1; - workerOption.MaxMessagesPerTask = 1; - - workers[i] = new ActionBlock(action, workerOption); - } - - var distributorOption = new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - BoundedCapacity = 1 - }; - - distributor = new ActionBlock(x => - { - var partition = Math.Abs(partitioner(x)) % workers.Length; - - return workers[partition].SendAsync(x); - }, distributorOption); - - distributor.Completion.ContinueWith(x => - { - foreach (var worker in workers) - { - worker.Complete(); - } - }); - } - - public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock source, bool consumeToAccept) - { - return distributor.OfferMessage(messageHeader, messageValue, source, consumeToAccept); - } - - public void Complete() - { - distributor.Complete(); - } - - public void Fault(Exception exception) - { - distributor.Fault(exception); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs b/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs deleted file mode 100644 index f56f1ac46..000000000 --- a/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; - -namespace Squidex.Infrastructure.Tasks -{ - public sealed class SingleThreadedDispatcher - { - private readonly ActionBlock> block; - private bool isStopped; - - public SingleThreadedDispatcher(int capacity = 1) - { - var options = new ExecutionDataflowBlockOptions - { - BoundedCapacity = capacity, - MaxMessagesPerTask = 1, - MaxDegreeOfParallelism = 1 - }; - - block = new ActionBlock>(Handle, options); - } - - public Task DispatchAsync(Func action) - { - Guard.NotNull(action, nameof(action)); - - return block.SendAsync(action); - } - - public Task DispatchAsync(Action action) - { - Guard.NotNull(action, nameof(action)); - - return block.SendAsync(() => { action(); return TaskHelper.Done; }); - } - - public async Task StopAndWaitAsync() - { - await DispatchAsync(() => - { - isStopped = true; - - block.Complete(); - }); - - await block.Completion; - } - - private Task Handle(Func action) - { - if (isStopped) - { - return TaskHelper.Done; - } - - return action(); - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs deleted file mode 100644 index 6f7bf787c..000000000 --- a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Tasks -{ - public static class TaskExtensions - { - private static readonly Action IgnoreTaskContinuation = t => { var ignored = t.Exception; }; - - public static void Forget(this Task task) - { - if (task.IsCompleted) - { - var ignored = task.Exception; - } - else - { - task.ContinueWith( - IgnoreTaskContinuation, - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted | - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - } - - public static Func ToDefault(this Action action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - action(x); - - return default; - }; - } - - public static Func> ToDefault(this Func action) - { - Guard.NotNull(action, nameof(action)); - - return async x => - { - await action(x); - - return default; - }; - } - - public static Func> ToAsync(this Func action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - var result = action(x); - - return Task.FromResult(result); - }; - } - - public static Func ToAsync(this Action action) - { - return x => - { - action(x); - - return TaskHelper.Done; - }; - } - - public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (cancellationToken.Register(state => - { - ((TaskCompletionSource)state).TrySetResult(null); - }, - tcs)) - { - var resultTask = await Task.WhenAny(task, tcs.Task); - if (resultTask == tcs.Task) - { - throw new OperationCanceledException(cancellationToken); - } - - return await task; - } - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs deleted file mode 100644 index 6c59a1d37..000000000 --- a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Tasks -{ - public static class TaskHelper - { - public static readonly Task Done = CreateDoneTask(); - public static readonly Task False = CreateResultTask(false); - public static readonly Task True = CreateResultTask(true); - - private static Task CreateDoneTask() - { - var result = new TaskCompletionSource(); - - result.SetResult(null); - - return result.Task; - } - - private static Task CreateResultTask(bool value) - { - var result = new TaskCompletionSource(); - - result.SetResult(value); - - return result.Task; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs deleted file mode 100644 index a61a136d0..000000000 --- a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Timers -{ - public sealed class CompletionTimer - { - private const int OneCallNotExecuted = 0; - private const int OneCallExecuted = 1; - private const int OneCallRequested = 2; - private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); - private readonly Task runTask; - private int oneCallState; - private CancellationTokenSource wakeupToken; - - public CompletionTimer(int delayInMs, Func callback, int initialDelay = 0) - { - Guard.NotNull(callback, nameof(callback)); - Guard.GreaterThan(delayInMs, 0, nameof(delayInMs)); - - runTask = RunInternalAsync(delayInMs, initialDelay, callback); - } - - public Task StopAsync() - { - stopToken.Cancel(); - - return runTask; - } - - public void SkipCurrentDelay() - { - if (!stopToken.IsCancellationRequested) - { - Interlocked.CompareExchange(ref oneCallState, OneCallRequested, OneCallNotExecuted); - - wakeupToken?.Cancel(); - } - } - - private async Task RunInternalAsync(int delay, int initialDelay, Func callback) - { - try - { - if (initialDelay > 0) - { - await WaitAsync(initialDelay).ConfigureAwait(false); - } - - while (oneCallState == OneCallRequested || !stopToken.IsCancellationRequested) - { - await callback(stopToken.Token).ConfigureAwait(false); - - oneCallState = OneCallExecuted; - - await WaitAsync(delay).ConfigureAwait(false); - } - } - catch - { - return; - } - } - - private async Task WaitAsync(int intervall) - { - try - { - wakeupToken = new CancellationTokenSource(); - - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, wakeupToken.Token)) - { - await Task.Delay(intervall, cts.Token).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - } - } - } -} diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs deleted file mode 100644 index 5409faca1..000000000 --- a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.Json; - -namespace Squidex.Infrastructure.Translations -{ - public sealed class DeepLTranslator : ITranslator - { - private const string Url = "https://api.deepl.com/v2/translate"; - private readonly HttpClient httpClient = new HttpClient(); - private readonly DeepLTranslatorOptions deepLOptions; - private readonly IJsonSerializer jsonSerializer; - - private sealed class Response - { - public ResponseTranslation[] Translations { get; set; } - } - - private sealed class ResponseTranslation - { - public string Text { get; set; } - } - - public DeepLTranslator(IOptions deepLOptions, IJsonSerializer jsonSerializer) - { - Guard.NotNull(deepLOptions, nameof(deepLOptions)); - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - this.deepLOptions = deepLOptions.Value; - - this.jsonSerializer = jsonSerializer; - } - - public async Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(sourceText) || targetLanguage == null) - { - return new Translation(TranslationResult.NotTranslated, sourceText); - } - - if (string.IsNullOrWhiteSpace(deepLOptions.AuthKey)) - { - return new Translation(TranslationResult.NotImplemented); - } - - var parameters = new Dictionary - { - ["auth_key"] = deepLOptions.AuthKey, - ["text"] = sourceText, - ["target_lang"] = GetLanguageCode(targetLanguage) - }; - - if (sourceLanguage != null) - { - parameters["source_lang"] = GetLanguageCode(sourceLanguage); - } - - var response = await httpClient.PostAsync(Url, new FormUrlEncodedContent(parameters), ct); - var responseString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - var result = jsonSerializer.Deserialize(responseString); - - if (result?.Translations?.Length == 1) - { - return new Translation(TranslationResult.Translated, result.Translations[0].Text); - } - } - - if (response.StatusCode == HttpStatusCode.BadRequest) - { - return new Translation(TranslationResult.LanguageNotSupported, resultText: responseString); - } - - return new Translation(TranslationResult.Failed, resultText: responseString); - } - - private static string GetLanguageCode(Language language) - { - return language.Iso2Code.Substring(0, 2).ToUpperInvariant(); - } - } -} diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs deleted file mode 100644 index d7124e343..000000000 --- a/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Translations -{ - public sealed class DeepLTranslatorOptions - { - public string AuthKey { get; set; } - } -} diff --git a/src/Squidex.Infrastructure/Translations/ITranslator.cs b/src/Squidex.Infrastructure/Translations/ITranslator.cs deleted file mode 100644 index 456923bb4..000000000 --- a/src/Squidex.Infrastructure/Translations/ITranslator.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Translations -{ - public interface ITranslator - { - Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default); - } -} diff --git a/src/Squidex.Infrastructure/Translations/NoopTranslator.cs b/src/Squidex.Infrastructure/Translations/NoopTranslator.cs deleted file mode 100644 index e872675c9..000000000 --- a/src/Squidex.Infrastructure/Translations/NoopTranslator.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Translations -{ - public sealed class NoopTranslator : ITranslator - { - public Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) - { - var result = new Translation(TranslationResult.NotImplemented); - - return Task.FromResult(result); - } - } -} diff --git a/src/Squidex.Infrastructure/Translations/Translation.cs b/src/Squidex.Infrastructure/Translations/Translation.cs deleted file mode 100644 index 7bcabad2a..000000000 --- a/src/Squidex.Infrastructure/Translations/Translation.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Translations -{ - public sealed class Translation - { - public TranslationResult Result { get; } - - public string Text { get; } - - public string ResultText { get; set; } - - public Translation(TranslationResult result, string text = null, string resultText = null) - { - Text = text; - Result = result; - ResultText = resultText; - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs deleted file mode 100644 index 70091fd1c..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.Timers; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker - { - public const string CounterTotalCalls = "TotalCalls"; - public const string CounterTotalElapsedMs = "TotalElapsedMs"; - - private const string FallbackCategory = "*"; - private const int Intervall = 60 * 1000; - private readonly IUsageRepository usageRepository; - private readonly ISemanticLog log; - private readonly CompletionTimer timer; - private ConcurrentDictionary<(string Key, string Category), Usage> usages = new ConcurrentDictionary<(string Key, string Category), Usage>(); - - public BackgroundUsageTracker(IUsageRepository usageRepository, ISemanticLog log) - { - Guard.NotNull(usageRepository, nameof(usageRepository)); - Guard.NotNull(log, nameof(log)); - - this.usageRepository = usageRepository; - - this.log = log; - - timer = new CompletionTimer(Intervall, ct => TrackAsync(), Intervall); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - timer.StopAsync().Wait(); - } - } - - public void Next() - { - ThrowIfDisposed(); - - timer.SkipCurrentDelay(); - } - - private async Task TrackAsync() - { - try - { - var today = DateTime.Today; - - var localUsages = Interlocked.Exchange(ref usages, new ConcurrentDictionary<(string Key, string Category), Usage>()); - - if (localUsages.Count > 0) - { - var updates = new UsageUpdate[localUsages.Count]; - var updateIndex = 0; - - foreach (var kvp in localUsages) - { - var counters = new Counters - { - [CounterTotalCalls] = kvp.Value.Count, - [CounterTotalElapsedMs] = kvp.Value.ElapsedMs - }; - - updates[updateIndex].Key = kvp.Key.Key; - updates[updateIndex].Category = kvp.Key.Category; - updates[updateIndex].Counters = counters; - updates[updateIndex].Date = today; - - updateIndex++; - } - - await usageRepository.TrackUsagesAsync(updates); - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "TrackUsage") - .WriteProperty("status", "Failed")); - } - } - - public Task TrackAsync(string key, string category, double weight, double elapsedMs) - { - key = GetKey(key); - - ThrowIfDisposed(); - - if (weight > 0) - { - category = GetCategory(category); - - usages.AddOrUpdate((key, category), _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight)); - } - - return TaskHelper.Done; - } - - public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - key = GetKey(key); - - ThrowIfDisposed(); - - var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate); - var usagesByCategory = usagesFlat.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); - - var result = new Dictionary>(); - - IEnumerable categories = usagesByCategory.Keys; - - if (usagesByCategory.Count == 0) - { - var enriched = new List(); - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - enriched.Add(new DateUsage(date, 0, 0)); - } - - result[FallbackCategory] = enriched; - } - else - { - foreach (var category in categories) - { - var enriched = new List(); - - var usagesDictionary = usagesByCategory[category].ToDictionary(x => x.Date); - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - var stored = usagesDictionary.GetOrDefault(date); - - var totalCount = 0L; - var totalElapsedMs = 0L; - - if (stored != null) - { - totalCount = (long)stored.Counters.Get(CounterTotalCalls); - totalElapsedMs = (long)stored.Counters.Get(CounterTotalElapsedMs); - } - - enriched.Add(new DateUsage(date, totalCount, totalElapsedMs)); - } - - result[category] = enriched; - } - } - - return result; - } - - public Task GetMonthlyCallsAsync(string key, DateTime date) - { - return GetPreviousCallsAsync(key, new DateTime(date.Year, date.Month, 1), date); - } - - public async Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) - { - key = GetKey(key); - - ThrowIfDisposed(); - - var originalUsages = await usageRepository.QueryAsync(key, fromDate, toDate); - - return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); - } - - private static string GetCategory(string category) - { - return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; - } - - private static string GetKey(string key) - { - Guard.NotNull(key, nameof(key)); - - return $"{key}_API"; - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs deleted file mode 100644 index 2ba8ec8a1..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); - private readonly IUsageTracker inner; - - public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) - : base(cache) - { - Guard.NotNull(inner, nameof(inner)); - - this.inner = inner; - } - - public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - Guard.NotNull(key, nameof(key)); - - return inner.QueryAsync(key, fromDate, toDate); - } - - public Task TrackAsync(string key, string category, double weight, double elapsedMs) - { - Guard.NotNull(key, nameof(key)); - - return inner.TrackAsync(key, category, weight, elapsedMs); - } - - public Task GetMonthlyCallsAsync(string key, DateTime date) - { - Guard.NotNull(key, nameof(key)); - - var cacheKey = string.Join("$", "Usage", nameof(GetMonthlyCallsAsync), key, date); - - return Cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - return inner.GetMonthlyCallsAsync(key, date); - }); - } - - public Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) - { - Guard.NotNull(key, nameof(key)); - - var cacheKey = string.Join("$", "Usage", nameof(GetPreviousCallsAsync), key, fromDate, toDate); - - return Cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; - - return inner.GetPreviousCallsAsync(key, fromDate, toDate); - }); - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs deleted file mode 100644 index bb94d83ff..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; - -namespace Squidex.Infrastructure.UsageTracking -{ - public interface IUsageTracker - { - Task TrackAsync(string key, string category, double weight, double elapsedMs); - - Task GetMonthlyCallsAsync(string key, DateTime date); - - Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate); - - Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs b/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs deleted file mode 100644 index 6a84b4129..000000000 --- a/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class StoredUsage - { - public string Category { get; } - - public DateTime Date { get; } - - public Counters Counters { get; } - - public StoredUsage(string category, DateTime date, Counters counters) - { - Guard.NotNull(counters, nameof(counters)); - - Category = category; - Counters = counters; - - Date = date; - } - } -} diff --git a/src/Squidex.Infrastructure/Validation/Validate.cs b/src/Squidex.Infrastructure/Validation/Validate.cs deleted file mode 100644 index 0b37d8a4a..000000000 --- a/src/Squidex.Infrastructure/Validation/Validate.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; - -namespace Squidex.Infrastructure.Validation -{ - public static class Validate - { - public static void It(Func message, Action action) - { - List errors = null; - - var addValidation = new AddValidation((m, p) => - { - if (errors == null) - { - errors = new List(); - } - - errors.Add(new ValidationError(m, p)); - }); - - action(addValidation); - - if (errors != null) - { - throw new ValidationException(message(), errors); - } - } - - public static async Task It(Func message, Func action) - { - List errors = null; - - var addValidation = new AddValidation((m, p) => - { - if (errors == null) - { - errors = new List(); - } - - errors.Add(new ValidationError(m, p)); - }); - - await action(addValidation); - - if (errors != null) - { - throw new ValidationException(message(), errors); - } - } - } - - public delegate void AddValidation(string message, params string[] propertyNames); -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Validation/ValidationError.cs b/src/Squidex.Infrastructure/Validation/ValidationError.cs deleted file mode 100644 index 21db20f6c..000000000 --- a/src/Squidex.Infrastructure/Validation/ValidationError.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// 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; - -namespace Squidex.Infrastructure.Validation -{ - [Serializable] - public sealed class ValidationError - { - private readonly string message; - private readonly string[] propertyNames; - - public string Message - { - get { return message; } - } - - public IEnumerable PropertyNames - { - get { return propertyNames; } - } - - public ValidationError(string message, params string[] propertyNames) - { - Guard.NotNullOrEmpty(message, nameof(message)); - - this.message = message; - - this.propertyNames = propertyNames ?? Array.Empty(); - } - - public ValidationError WithPrefix(string prefix) - { - if (propertyNames.Length > 0) - { - return new ValidationError(Message, propertyNames.Select(x => $"{prefix}.{x}").ToArray()); - } - else - { - return new ValidationError(Message, prefix); - } - } - - public void AddTo(AddValidation e) - { - e(Message, propertyNames); - } - } -} diff --git a/src/Squidex.Infrastructure/Validation/ValidationException.cs b/src/Squidex.Infrastructure/Validation/ValidationException.cs deleted file mode 100644 index 3ad48a3c7..000000000 --- a/src/Squidex.Infrastructure/Validation/ValidationException.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// 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 System.Runtime.Serialization; -using System.Text; - -namespace Squidex.Infrastructure.Validation -{ - [Serializable] - public class ValidationException : DomainException - { - private static readonly List FallbackErrors = new List(); - private readonly IReadOnlyList errors; - - public IReadOnlyList Errors - { - get { return errors ?? FallbackErrors; } - } - - public string Summary { get; } - - public ValidationException(string summary, params ValidationError[] errors) - : this(summary, null, errors?.ToList()) - { - } - - public ValidationException(string summary, IReadOnlyList errors) - : this(summary, null, errors) - { - this.errors = errors ?? FallbackErrors; - } - - public ValidationException(string summary, Exception inner, params ValidationError[] errors) - : this(summary, inner, errors?.ToList()) - { - } - - public ValidationException(string summary, Exception inner, IReadOnlyList errors) - : base(FormatMessage(summary, errors), inner) - { - Summary = summary; - - this.errors = errors ?? FallbackErrors; - } - - protected ValidationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Summary = info.GetString(nameof(Summary)); - - errors = (List)info.GetValue(nameof(errors), typeof(List)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Summary), Summary); - info.AddValue(nameof(errors), errors.ToList()); - - base.GetObjectData(info, context); - } - - private static string FormatMessage(string summary, IReadOnlyList errors) - { - var sb = new StringBuilder(); - - sb.Append(summary.TrimEnd(' ', '.', ':')); - - if (errors?.Count > 0) - { - sb.Append(": "); - - for (var i = 0; i < errors.Count; i++) - { - var error = errors[i]?.Message; - - if (!string.IsNullOrWhiteSpace(error)) - { - sb.Append(error); - - if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) - { - sb.Append("."); - } - - if (i < errors.Count - 1) - { - sb.Append(" "); - } - } - } - } - else - { - sb.Append("."); - } - - return sb.ToString(); - } - } -} diff --git a/src/Squidex.Shared/Permissions.cs b/src/Squidex.Shared/Permissions.cs deleted file mode 100644 index 1d7e6bdd0..000000000 --- a/src/Squidex.Shared/Permissions.cs +++ /dev/null @@ -1,184 +0,0 @@ -// ========================================================================== -// 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 System.Reflection; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; - -namespace Squidex.Shared -{ - public static class Permissions - { - private static readonly List ForAppsNonSchemaList = new List(); - private static readonly List ForAppsSchemaList = new List(); - - public static IReadOnlyList ForAppsNonSchema - { - get { return ForAppsNonSchemaList; } - } - - public static IReadOnlyList ForAppsSchema - { - get { return ForAppsSchemaList; } - } - - public const string All = "squidex.*"; - - public const string Admin = "squidex.admin.*"; - public const string AdminOrleans = "squidex.admin.orleans"; - - public const string AdminAppCreate = "squidex.admin.apps.create"; - - public const string AdminRestore = "squidex.admin.restore"; - - public const string AdminEvents = "squidex.admin.events"; - public const string AdminEventsRead = "squidex.admin.events.read"; - public const string AdminEventsManage = "squidex.admin.events.manage"; - - public const string AdminUsers = "squidex.admin.users"; - public const string AdminUsersRead = "squidex.admin.users.read"; - public const string AdminUsersCreate = "squidex.admin.users.create"; - public const string AdminUsersUpdate = "squidex.admin.users.update"; - public const string AdminUsersUnlock = "squidex.admin.users.unlock"; - public const string AdminUsersLock = "squidex.admin.users.lock"; - - public const string App = "squidex.apps.{app}"; - public const string AppCommon = "squidex.apps.{app}.common"; - - public const string AppDelete = "squidex.apps.{app}.delete"; - public const string AppUpdate = "squidex.apps.{app}.update"; - public const string AppUpdateImage = "squidex.apps.{app}.update"; - public const string AppUpdateGeneral = "squidex.apps.{app}.general"; - - public const string AppClients = "squidex.apps.{app}.clients"; - public const string AppClientsRead = "squidex.apps.{app}.clients.read"; - public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; - public const string AppClientsUpdate = "squidex.apps.{app}.clients.update"; - public const string AppClientsDelete = "squidex.apps.{app}.clients.delete"; - - public const string AppContributors = "squidex.apps.{app}.contributors"; - public const string AppContributorsRead = "squidex.apps.{app}.contributors.read"; - public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign"; - public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; - - public const string AppLanguages = "squidex.apps.{app}.languages"; - public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; - public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; - public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; - - public const string AppRoles = "squidex.apps.{app}.roles"; - public const string AppRolesRead = "squidex.apps.{app}.roles.read"; - public const string AppRolesCreate = "squidex.apps.{app}.roles.create"; - public const string AppRolesUpdate = "squidex.apps.{app}.roles.update"; - public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; - - public const string AppPatterns = "squidex.apps.{app}.patterns"; - public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create"; - public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; - public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; - - public const string AppWorkflows = "squidex.apps.{app}.workflows"; - public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read"; - public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create"; - public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update"; - public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete"; - - public const string AppBackups = "squidex.apps.{app}.backups"; - public const string AppBackupsRead = "squidex.apps.{app}.backups.read"; - public const string AppBackupsCreate = "squidex.apps.{app}.backups.create"; - public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete"; - - public const string AppPlans = "squidex.apps.{app}.plans"; - public const string AppPlansRead = "squidex.apps.{app}.plans.read"; - public const string AppPlansChange = "squidex.apps.{app}.plans.change"; - - public const string AppAssets = "squidex.apps.{app}.assets"; - public const string AppAssetsRead = "squidex.apps.{app}.assets.read"; - public const string AppAssetsCreate = "squidex.apps.{app}.assets.create"; - public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; - public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; - - public const string AppRules = "squidex.apps.{app}.rules"; - public const string AppRulesRead = "squidex.apps.{app}.rules.read"; - public const string AppRulesEvents = "squidex.apps.{app}.rules.events"; - public const string AppRulesCreate = "squidex.apps.{app}.rules.create"; - public const string AppRulesUpdate = "squidex.apps.{app}.rules.update"; - public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; - public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; - - public const string AppSchemas = "squidex.apps.{app}.schemas.{name}"; - public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create"; - public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; - public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; - public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; - public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete"; - - public const string AppContents = "squidex.apps.{app}.contents.{name}"; - public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; - public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; - public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; - public const string AppContentsDraftDiscard = "squidex.apps.{app}.contents.{name}.draft.discard"; - public const string AppContentsDraftPublish = "squidex.apps.{app}.contents.{name}.draft.publish"; - public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; - - public const string AppApi = "squidex.apps.{app}.api"; - - static Permissions() - { - foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) - { - if (field.IsLiteral && !field.IsInitOnly) - { - var value = (string)field.GetValue(null); - - if (value.StartsWith(App, StringComparison.OrdinalIgnoreCase)) - { - if (value.IndexOf("{name}", App.Length, StringComparison.OrdinalIgnoreCase) >= 0) - { - ForAppsSchemaList.Add(value); - } - else - { - ForAppsNonSchemaList.Add(value); - } - } - } - } - } - - public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) - { - Guard.NotNull(id, nameof(id)); - - return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); - } - - public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app) - { - var matching = permissions.Where(x => x.StartsWith($"squidex.apps.{app}")); - - return new PermissionSet(matching); - } - - public static string[] ToAppNames(this PermissionSet permissions) - { - var matching = permissions.Where(x => x.StartsWith("squidex.apps.")); - - var result = - matching - .Select(x => x.Id.Split('.')).Where(x => x.Length > 2) - .Select(x => x[2]) - .Distinct() - .ToArray(); - - return result; - } - } -} diff --git a/src/Squidex.Shared/Squidex.Shared.csproj b/src/Squidex.Shared/Squidex.Shared.csproj deleted file mode 100644 index 51c30aaf0..000000000 --- a/src/Squidex.Shared/Squidex.Shared.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - ..\..\Squidex.ruleset - - - - - - - - \ No newline at end of file diff --git a/src/Squidex.Shared/Users/ClientUser.cs b/src/Squidex.Shared/Users/ClientUser.cs deleted file mode 100644 index 88f840eff..000000000 --- a/src/Squidex.Shared/Users/ClientUser.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Security.Claims; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; - -namespace Squidex.Shared.Users -{ - public sealed class ClientUser : IUser - { - private readonly RefToken token; - private readonly List claims; - - public ClientUser(RefToken token) - { - Guard.NotNull(token, nameof(token)); - - this.token = token; - - claims = new List - { - new Claim(OpenIdClaims.ClientId, token.Identifier), - new Claim(SquidexClaimTypes.DisplayName, token.ToString()) - }; - } - - public string Id - { - get { return token.Identifier; } - } - - public string Email - { - get { return token.ToString(); } - } - - public bool IsLocked - { - get { return false; } - } - - public IReadOnlyList Claims - { - get { return claims; } - } - } -} diff --git a/src/Squidex.Shared/Users/IUserResolver.cs b/src/Squidex.Shared/Users/IUserResolver.cs deleted file mode 100644 index 6a3e8f1df..000000000 --- a/src/Squidex.Shared/Users/IUserResolver.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Shared.Users -{ - public interface IUserResolver - { - Task CreateUserIfNotExists(string email, bool invited = false); - - Task FindByIdOrEmailAsync(string idOrEmail); - - Task> QueryByEmailAsync(string email); - - Task> QueryManyAsync(string[] ids); - } -} diff --git a/src/Squidex.Shared/Users/UserExtensions.cs b/src/Squidex.Shared/Users/UserExtensions.cs deleted file mode 100644 index cc9adfd88..000000000 --- a/src/Squidex.Shared/Users/UserExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; - -namespace Squidex.Shared.Users -{ - public static class UserExtensions - { - public static PermissionSet Permissions(this IUser user) - { - return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); - } - - public static bool IsInvited(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); - } - - public static bool IsHidden(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); - } - - public static bool HasConsent(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); - } - - public static bool HasConsentForEmails(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); - } - - public static bool HasDisplayName(this IUser user) - { - return user.HasClaim(SquidexClaimTypes.DisplayName); - } - - public static bool HasPictureUrl(this IUser user) - { - return user.HasClaim(SquidexClaimTypes.PictureUrl); - } - - public static bool IsPictureUrlStored(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); - } - - public static string ClientSecret(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.ClientSecret); - } - - public static string PictureUrl(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.PictureUrl); - } - - public static string DisplayName(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.DisplayName); - } - - public static string GetClaimValue(this IUser user, string type) - { - return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; - } - - public static string[] GetClaimValues(this IUser user, string type) - { - return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray(); - } - - public static bool HasClaim(this IUser user, string type) - { - return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - } - - public static bool HasClaimValue(this IUser user, string type, string value) - { - return user.Claims.Any(x => - string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && - string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); - } - - public static string PictureNormalizedUrl(this IUser user) - { - var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; - - if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) - { - if (url.Contains("?")) - { - url += "&d=404"; - } - else - { - url += "?d=404"; - } - } - - return url; - } - } -} diff --git a/src/Squidex.Web/ApiController.cs b/src/Squidex.Web/ApiController.cs deleted file mode 100644 index a7ead50d9..000000000 --- a/src/Squidex.Web/ApiController.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Web -{ - [Area("Api")] - [ApiController] - [ApiExceptionFilter] - [ApiModelValidation(false)] - public abstract class ApiController : Controller - { - protected ICommandBus CommandBus { get; } - - protected IAppEntity App - { - get - { - var app = HttpContext.Context().App; - - if (app == null) - { - throw new InvalidOperationException("Not in a app context."); - } - - return app; - } - } - - protected Context Context - { - get { return HttpContext.Context(); } - } - - protected Guid AppId - { - get { return App.Id; } - } - - protected ApiController(ICommandBus commandBus) - { - Guard.NotNull(commandBus, nameof(commandBus)); - - CommandBus = commandBus; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - var request = context.HttpContext.Request; - - if (!request.PathBase.HasValue || !request.PathBase.Value.EndsWith("/api", StringComparison.OrdinalIgnoreCase)) - { - context.Result = new RedirectResult("/"); - } - } - } -} diff --git a/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/src/Squidex.Web/ApiExceptionFilterAttribute.cs deleted file mode 100644 index 352ea93e0..000000000 --- a/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// 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 System.Security; -using System.Text; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Web -{ - public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter - { - private static readonly List> Handlers = new List>(); - - private static void AddHandler(Func handler) where T : Exception - { - Handlers.Add(ex => ex is T typed ? handler(typed) : null); - } - - static ApiExceptionFilterAttribute() - { - AddHandler(OnValidationException); - AddHandler(OnDecoderException); - AddHandler(OnDomainObjectNotFoundException); - AddHandler(OnDomainObjectVersionException); - AddHandler(OnDomainForbiddenException); - AddHandler(OnDomainException); - AddHandler(OnSecurityException); - } - - private static IActionResult OnDecoderException(DecoderFallbackException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainObjectNotFoundException(DomainObjectNotFoundException ex) - { - return new NotFoundResult(); - } - - private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) - { - return ErrorResult(412, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainException(DomainException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex) - { - return ErrorResult(403, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnSecurityException(SecurityException ex) - { - return ErrorResult(403, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnValidationException(ValidationException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) }); - } - - private static IActionResult ErrorResult(int statusCode, ErrorDto error) - { - error.StatusCode = statusCode; - - return new ObjectResult(error) { StatusCode = statusCode }; - } - - public void OnException(ExceptionContext context) - { - IActionResult result = null; - - foreach (var handler in Handlers) - { - result = handler(context.Exception); - - if (result != null) - { - break; - } - } - - if (result != null) - { - context.Result = result; - } - } - - private static string[] ToDetails(ValidationException ex) - { - return ex.Errors?.Select(e => - { - if (e.PropertyNames?.Any() == true) - { - return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; - } - else - { - return e.Message; - } - }).ToArray(); - } - } -} diff --git a/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs b/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs deleted file mode 100644 index 2b6aabf22..000000000 --- a/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Assets; - -namespace Squidex.Web -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public sealed class AssetRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter - { - public int Order { get; set; } = 900; - - public bool IsReusable => true; - - public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) - { - var assetOptions = serviceProvider.GetService>(); - - if (assetOptions?.Value.MaxSize > 0) - { - var filter = serviceProvider.GetRequiredService(); - - filter.Bytes = assetOptions.Value.MaxSize; - - return filter; - } - else - { - var filter = serviceProvider.GetRequiredService(); - - return filter; - } - } - } -} diff --git a/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs deleted file mode 100644 index c837b5dd0..000000000 --- a/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Web.CommandMiddlewares -{ - public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware - { - private readonly IAppProvider appProvider; - private readonly IActionContextAccessor actionContextAccessor; - - public EnrichWithSchemaIdCommandMiddleware(IAppProvider appProvider, IActionContextAccessor actionContextAccessor) - { - this.appProvider = appProvider; - - this.actionContextAccessor = actionContextAccessor; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (actionContextAccessor.ActionContext == null) - { - await next(); - - return; - } - - if (context.Command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) - { - var schemaId = await GetSchemaIdAsync(context); - - schemaCommand.SchemaId = schemaId; - } - - if (context.Command is SchemaCommand schemaSelfCommand && schemaSelfCommand.SchemaId == Guid.Empty) - { - var schemaId = await GetSchemaIdAsync(context); - - schemaSelfCommand.SchemaId = schemaId?.Id ?? Guid.Empty; - } - - await next(); - } - - private async Task> GetSchemaIdAsync(CommandContext context) - { - NamedId appId = null; - - if (context.Command is IAppCommand appCommand) - { - appId = appCommand.AppId; - } - - if (appId == null) - { - appId = actionContextAccessor.ActionContext.HttpContext.Context().App?.NamedId(); - } - - if (appId != null) - { - var routeValues = actionContextAccessor.ActionContext.RouteData.Values; - - if (routeValues.ContainsKey("name")) - { - var schemaName = routeValues["name"].ToString(); - - ISchemaEntity schema; - - if (Guid.TryParse(schemaName, out var id)) - { - schema = await appProvider.GetSchemaAsync(appId.Id, id); - } - else - { - schema = await appProvider.GetSchemaAsync(appId.Id, schemaName); - } - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaName, typeof(ISchemaEntity)); - } - - return schema.NamedId(); - } - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Web/ContextProvider.cs b/src/Squidex.Web/ContextProvider.cs deleted file mode 100644 index 6d960b7eb..000000000 --- a/src/Squidex.Web/ContextProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public sealed class ContextProvider : IContextProvider - { - private readonly IHttpContextAccessor httpContextAccessor; - private readonly AsyncLocal asyncLocal = new AsyncLocal(); - - public Context Context - { - get - { - if (httpContextAccessor.HttpContext == null) - { - if (asyncLocal.Value == null) - { - asyncLocal.Value = Context.Anonymous(); - } - - return asyncLocal.Value; - } - - return httpContextAccessor.HttpContext.Context(); - } - } - - public ContextProvider(IHttpContextAccessor httpContextAccessor) - { - Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); - - this.httpContextAccessor = httpContextAccessor; - } - } -} diff --git a/src/Squidex.Web/Deferred.cs b/src/Squidex.Web/Deferred.cs deleted file mode 100644 index 717182f49..000000000 --- a/src/Squidex.Web/Deferred.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public struct Deferred - { - private readonly Lazy> value; - - public Task Value - { - get { return value.Value; } - } - - private Deferred(Func> value) - { - this.value = new Lazy>(value); - } - - public static Deferred Response(Func factory) - { - Guard.NotNull(factory, nameof(factory)); - - return new Deferred(() => Task.FromResult(factory())); - } - - public static Deferred AsyncResponse(Func> factory) - { - Guard.NotNull(factory, nameof(factory)); - - return new Deferred(async () => await factory()); - } - } -} diff --git a/src/Squidex.Web/EntityCreatedDto.cs b/src/Squidex.Web/EntityCreatedDto.cs deleted file mode 100644 index 754f33f77..000000000 --- a/src/Squidex.Web/EntityCreatedDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Web -{ - public sealed class EntityCreatedDto - { - [Required] - [Display(Description = "Id of the created entity.")] - public string Id { get; set; } - - [Display(Description = "The new version of the entity.")] - public long Version { get; set; } - - public static EntityCreatedDto FromResult(EntityCreatedResult result) - { - return new EntityCreatedDto { Id = result.IdOrValue?.ToString(), Version = result.Version }; - } - } -} diff --git a/src/Squidex.Web/ExposedValues.cs b/src/Squidex.Web/ExposedValues.cs deleted file mode 100644 index 4b56935cd..000000000 --- a/src/Squidex.Web/ExposedValues.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using Microsoft.Extensions.Configuration; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public sealed class ExposedValues : Dictionary - { - public ExposedValues() - { - } - - public ExposedValues(ExposedConfiguration configured, IConfiguration configuration, Assembly assembly = null) - { - Guard.NotNull(configured, nameof(configured)); - Guard.NotNull(configuration, nameof(configuration)); - - foreach (var kvp in configured) - { - var value = configuration.GetValue(kvp.Value); - - if (!string.IsNullOrWhiteSpace(value)) - { - this[kvp.Key] = value; - } - } - - if (assembly != null) - { - if (!ContainsKey("version")) - { - this["version"] = assembly.GetName().Version.ToString(); - } - } - } - - public override string ToString() - { - var sb = new StringBuilder(); - - foreach (var kvp in this) - { - if (sb.Length > 0) - { - sb.Append(", "); - } - - sb.Append(kvp.Key); - sb.Append(": "); - sb.Append(kvp.Value); - } - - return sb.ToString(); - } - } -} diff --git a/src/Squidex.Web/Extensions.cs b/src/Squidex.Web/Extensions.cs deleted file mode 100644 index d2c2fec97..000000000 --- a/src/Squidex.Web/Extensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Microsoft.AspNetCore.Http; -using Squidex.Infrastructure.Security; - -namespace Squidex.Web -{ - public static class Extensions - { - public static string GetClientId(this ClaimsPrincipal principal) - { - var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; - - return clientId?.GetClientParts().ClientId; - } - - public static (string App, string ClientId) GetClientParts(this string clientId) - { - var parts = clientId.Split(':', '~'); - - if (parts.Length == 1) - { - return (null, parts[0]); - } - - if (parts.Length == 2) - { - return (parts[0], parts[1]); - } - - return (null, null); - } - - public static bool IsUser(this ApiController controller, string userId) - { - var subject = controller.User.OpenIdSubject(); - - return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); - } - - public static bool TryGetHeaderString(this IHeaderDictionary headers, string header, out string result) - { - result = null; - - if (headers.TryGetValue(header, out var value)) - { - string valueString = value; - - if (!string.IsNullOrWhiteSpace(valueString)) - { - result = valueString; - return true; - } - } - - return false; - } - } -} diff --git a/src/Squidex.Web/FileCallbackResult.cs b/src/Squidex.Web/FileCallbackResult.cs deleted file mode 100644 index 5ca752eea..000000000 --- a/src/Squidex.Web/FileCallbackResult.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Web.Pipeline; - -namespace Squidex.Web -{ - public sealed class FileCallbackResult : FileResult - { - public bool Send404 { get; } - - public Func Callback { get; } - - public FileCallbackResult(string contentType, string name, bool send404, Func callback) - : base(contentType) - { - FileDownloadName = name; - - Send404 = send404; - - Callback = callback; - } - - public override Task ExecuteResultAsync(ActionContext context) - { - var executor = context.HttpContext.RequestServices.GetRequiredService(); - - return executor.ExecuteAsync(context, this); - } - } -} - -#pragma warning restore 1573 \ No newline at end of file diff --git a/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs deleted file mode 100644 index f87d632fd..000000000 --- a/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// 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 System.Reflection; -using System.Runtime.Serialization; -using Newtonsoft.Json.Linq; -using NJsonSchema.Converters; -using Squidex.Infrastructure; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Web.Json -{ - public class TypedJsonInheritanceConverter : JsonInheritanceConverter - { - private static readonly Lazy> DefaultMapping = new Lazy>(() => - { - var baseName = typeof(T).Name; - - var result = new Dictionary(); - - void AddType(Type type) - { - var discriminator = type.Name; - - if (discriminator.EndsWith(baseName, StringComparison.CurrentCulture)) - { - discriminator = discriminator.Substring(0, discriminator.Length - baseName.Length); - } - - result[discriminator] = type; - } - - foreach (var attribute in typeof(T).GetCustomAttributes()) - { - if (attribute.Type != null) - { - if (!attribute.Type.IsAbstract) - { - AddType(attribute.Type); - } - } - else if (!string.IsNullOrWhiteSpace(attribute.MethodName)) - { - var method = typeof(T).GetMethod(attribute.MethodName); - - if (method != null && method.IsStatic) - { - var types = (IEnumerable)method.Invoke(null, new object[0]); - - foreach (var type in types) - { - if (!type.IsAbstract) - { - AddType(type); - } - } - } - } - } - - return result; - }); - - private readonly IReadOnlyDictionary maping; - - public TypedJsonInheritanceConverter(string discriminator) - : this(discriminator, DefaultMapping.Value) - { - } - - public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) - : base(typeof(T), discriminator) - { - maping = mapping ?? DefaultMapping.Value; - } - - protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) - { - return maping.GetOrDefault(discriminatorValue) ?? throw new InvalidOperationException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'."); - } - - public override string GetDiscriminatorValue(Type type) - { - return maping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Web/PermissionExtensions.cs b/src/Squidex.Web/PermissionExtensions.cs deleted file mode 100644 index 68a2df38a..000000000 --- a/src/Squidex.Web/PermissionExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Http; -using Squidex.Infrastructure.Security; -using AllPermissions = Squidex.Shared.Permissions; - -namespace Squidex.Web -{ - public static class PermissionExtensions - { - public static PermissionSet Permissions(this HttpContext httpContext) - { - return httpContext.Context().Permissions; - } - - public static bool Includes(this HttpContext httpContext, Permission permission, PermissionSet additional = null) - { - return httpContext.Permissions().Includes(permission) || additional?.Includes(permission) == true; - } - - public static bool Includes(this ApiController controller, Permission permission, PermissionSet additional = null) - { - return controller.HttpContext.Includes(permission) || additional?.Includes(permission) == true; - } - - public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet additional = null) - { - return httpContext.Permissions().Allows(permission) || additional?.Allows(permission) == true; - } - - public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet additional = null) - { - return controller.HttpContext.HasPermission(permission) || additional?.Allows(permission) == true; - } - - public static bool HasPermission(this ApiController controller, string id, string app = Permission.Any, string schema = Permission.Any, PermissionSet additional = null) - { - if (app == Permission.Any) - { - if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s) - { - app = s; - } - } - - if (schema == Permission.Any) - { - if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s) - { - schema = s; - } - } - - var permission = AllPermissions.ForApp(id, app, schema); - - return controller.HasPermission(permission, additional); - } - } -} diff --git a/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/src/Squidex.Web/Pipeline/ApiCostsFilter.cs deleted file mode 100644 index 9f1241dad..000000000 --- a/src/Squidex.Web/Pipeline/ApiCostsFilter.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.UsageTracking; - -namespace Squidex.Web.Pipeline -{ - public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer - { - private readonly IAppPlansProvider appPlansProvider; - private readonly IUsageTracker usageTracker; - - public ApiCostsFilter(IAppPlansProvider appPlansProvider, IUsageTracker usageTracker) - { - this.appPlansProvider = appPlansProvider; - - this.usageTracker = usageTracker; - } - - IFilterMetadata IFilterContainer.FilterDefinition { get; set; } - - public ApiCostsAttribute FilterDefinition - { - get - { - return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; - } - set - { - ((IFilterContainer)this).FilterDefinition = value; - } - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - context.HttpContext.Features.Set(FilterDefinition); - - var app = context.HttpContext.Context().App; - - if (app != null && FilterDefinition.Weight > 0) - { - var appId = app.Id.ToString(); - - using (Profiler.Trace("CheckUsage")) - { - var plan = appPlansProvider.GetPlanForApp(app); - - var usage = await usageTracker.GetMonthlyCallsAsync(appId, DateTime.Today); - - if (plan.MaxApiCalls >= 0 && usage > plan.MaxApiCalls * 1.1) - { - context.Result = new StatusCodeResult(429); - return; - } - } - - var watch = ValueStopwatch.StartNew(); - - try - { - await next(); - } - finally - { - var elapsedMs = watch.Stop(); - - await usageTracker.TrackAsync(appId, context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs); - } - } - else - { - await next(); - } - } - } -} diff --git a/src/Squidex.Web/Pipeline/AppResolver.cs b/src/Squidex.Web/Pipeline/AppResolver.cs deleted file mode 100644 index 33782219b..000000000 --- a/src/Squidex.Web/Pipeline/AppResolver.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; - -namespace Squidex.Web.Pipeline -{ - public sealed class AppResolver : IAsyncActionFilter - { - private readonly IAppProvider appProvider; - - public AppResolver(IAppProvider appProvider) - { - this.appProvider = appProvider; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var user = context.HttpContext.User; - - var appName = context.RouteData.Values["app"]?.ToString(); - - if (!string.IsNullOrWhiteSpace(appName)) - { - var app = await appProvider.GetAppAsync(appName); - - if (app == null) - { - context.Result = new NotFoundResult(); - return; - } - - var (role, permissions) = FindByOpenIdSubject(app, user); - - if (permissions == null) - { - (role, permissions) = FindByOpenIdClient(app, user); - } - - if (permissions != null) - { - var identity = user.Identities.First(); - - if (!string.IsNullOrWhiteSpace(role)) - { - identity.AddClaim(new Claim(ClaimTypes.Role, role)); - } - - foreach (var permission in permissions) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); - } - } - - var requestContext = context.HttpContext.Context(); - - requestContext.App = app; - requestContext.UpdatePermissions(); - - if (!requestContext.Permissions.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context)) - { - context.Result = new NotFoundResult(); - return; - } - } - - await next(); - } - - private static bool AllowAnonymous(ActionExecutingContext context) - { - return context.ActionDescriptor.FilterDescriptors.Any(x => x.Filter is AllowAnonymousFilter); - } - - private static (string, PermissionSet) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user) - { - var clientId = user.GetClientId(); - - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role)) - { - return (client.Role, role.Permissions); - } - - return (null, null); - } - - private static (string, PermissionSet) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) - { - var subjectId = user.OpenIdSubject(); - - if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) - { - return (roleName, role.Permissions); - } - - return (null, null); - } - } -} diff --git a/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs b/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs deleted file mode 100644 index b9f050191..000000000 --- a/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; - -namespace Squidex.Web.Pipeline -{ - public sealed class LocalCacheMiddleware : IMiddleware - { - private readonly ILocalCache localCache; - - public LocalCacheMiddleware(ILocalCache localCache) - { - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - using (localCache.StartContext()) - { - await next(context); - } - } - } -} diff --git a/src/Squidex.Web/Resource.cs b/src/Squidex.Web/Resource.cs deleted file mode 100644 index 9719dfd1d..000000000 --- a/src/Squidex.Web/Resource.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; -using Squidex.Infrastructure; - -namespace Squidex.Web -{ - public abstract class Resource - { - [JsonProperty("_links")] - [Required] - [Display(Description = "The links.")] - public Dictionary Links { get; } = new Dictionary(); - - public void AddSelfLink(string href) - { - AddGetLink("self", href); - } - - public void AddGetLink(string rel, string href, string metadata = null) - { - AddLink(rel, "GET", href, metadata); - } - - public void AddPatchLink(string rel, string href, string metadata = null) - { - AddLink(rel, "PATCH", href, metadata); - } - - public void AddPostLink(string rel, string href, string metadata = null) - { - AddLink(rel, "POST", href, metadata); - } - - public void AddPutLink(string rel, string href, string metadata = null) - { - AddLink(rel, "PUT", href, metadata); - } - - public void AddDeleteLink(string rel, string href, string metadata = null) - { - AddLink(rel, "DELETE", href, metadata); - } - - public void AddLink(string rel, string method, string href, string metadata = null) - { - Guard.NotNullOrEmpty(rel, nameof(rel)); - Guard.NotNullOrEmpty(href, nameof(href)); - Guard.NotNullOrEmpty(method, nameof(method)); - - Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; - } - } -} diff --git a/src/Squidex.Web/ResourceLink.cs b/src/Squidex.Web/ResourceLink.cs deleted file mode 100644 index d1caffc8d..000000000 --- a/src/Squidex.Web/ResourceLink.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; - -namespace Squidex.Web -{ - public class ResourceLink - { - [Required] - [Display(Description = "The link url.")] - public string Href { get; set; } - - [Required] - [Display(Description = "The link method.")] - public string Method { get; set; } - - [Display(Description = "Additional data about the link.")] - public string Metadata { get; set; } - } -} diff --git a/src/Squidex.Web/Services/UrlGenerator.cs b/src/Squidex.Web/Services/UrlGenerator.cs deleted file mode 100644 index 5600beaaf..000000000 --- a/src/Squidex.Web/Services/UrlGenerator.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; - -namespace Squidex.Web.Services -{ - public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator, IEmailUrlGenerator - { - private readonly IAssetStore assetStore; - private readonly UrlsOptions urlsOptions; - - public bool CanGenerateAssetSourceUrl { get; } - - public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) - { - this.assetStore = assetStore; - this.urlsOptions = urlsOptions.Value; - - CanGenerateAssetSourceUrl = allowAssetSourceUrl; - } - - public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) - { - if (!asset.IsImage) - { - return null; - } - - return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}&width=100&mode=Max"); - } - - public string GenerateUrl(string assetId) - { - return urlsOptions.BuildUrl($"api/assets/{assetId}"); - } - - public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) - { - return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}"); - } - - public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) - { - return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.SchemaDef.Name}/{content.Id}"); - } - - public string GenerateContentUIUrl(NamedId appId, NamedId schemaId, Guid contentId) - { - return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history"); - } - - public string GenerateUIUrl() - { - return urlsOptions.BuildUrl("app/"); - } - - public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) - { - return assetStore.GeneratePublicUrl(asset.Id.ToString(), asset.FileVersion, null); - } - } -} diff --git a/src/Squidex.Web/Squidex.Web.csproj b/src/Squidex.Web/Squidex.Web.csproj deleted file mode 100644 index 2c9404cb7..000000000 --- a/src/Squidex.Web/Squidex.Web.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netstandard2.0 - 7.3 - - - full - True - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/src/Squidex.Web/UrlHelperExtensions.cs b/src/Squidex.Web/UrlHelperExtensions.cs deleted file mode 100644 index 8d59cba5f..000000000 --- a/src/Squidex.Web/UrlHelperExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc; - -#pragma warning disable RECS0108 // Warns about static fields in generic types - -namespace Squidex.Web -{ - public static class UrlHelperExtensions - { - private static class NameOf - { - public static readonly string Controller; - - static NameOf() - { - const string suffix = "Controller"; - - var name = typeof(T).Name; - - if (name.EndsWith(suffix, StringComparison.Ordinal)) - { - name = name.Substring(0, name.Length - suffix.Length); - } - - Controller = name; - } - } - - public static string Url(this IUrlHelper urlHelper, Func action, object values = null) where T : Controller - { - return urlHelper.Action(action(null), NameOf.Controller, values); - } - - public static string Url(this Controller controller, Func action, object values = null) where T : Controller - { - return controller.Url.Url(action, values); - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs deleted file mode 100644 index 018fddad9..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using Squidex.Web; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class CommonProcessor : IDocumentProcessor - { - private readonly string version; - private readonly string backgroundColor = "#3f83df"; - private readonly string logoUrl; - private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation - { - Url = "https://docs.squidex.io" - }; - - public CommonProcessor(ExposedValues exposedValues, IOptions urlOptions) - { - logoUrl = urlOptions.Value.BuildUrl("images/logo-white.png", false); - - if (!exposedValues.TryGetValue("version", out version)) - { - version = "1.0"; - } - } - - public void Process(DocumentProcessorContext context) - { - context.Document.BasePath = Constants.ApiPrefix; - - context.Document.Info.Version = version; - context.Document.Info.ExtensionData = new Dictionary - { - ["x-logo"] = new { url = logoUrl, backgroundColor } - }; - - context.Document.ExternalDocumentation = documentation; - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs b/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs deleted file mode 100644 index 9e6e9026f..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public static class OpenApiExtensions - { - public static void UseMyOpenApi(this IApplicationBuilder app) - { - app.UseOpenApi(); - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs deleted file mode 100644 index 0c7e3de7d..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using NJsonSchema; -using NJsonSchema.Generation.TypeMappers; -using NodaTime; -using NSwag.Generation; -using NSwag.Generation.Processors; -using Squidex.Areas.Api.Controllers.Contents.Generator; -using Squidex.Areas.Api.Controllers.Rules.Models; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public static class OpenApiServices - { - public static void AddMyOpenApiSettings(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddOpenApiDocument(settings => - { - settings.ConfigureName(); - settings.ConfigureSchemaSettings(); - - settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false)); - }); - - services.AddTransient(); - } - - public static void ConfigureName(this T settings) where T : OpenApiDocumentGeneratorSettings - { - settings.Title = "Squidex API"; - } - - public static void ConfigureSchemaSettings(this T settings) where T : OpenApiDocumentGeneratorSettings - { - settings.TypeMappers = new List - { - new PrimitiveTypeMapper(typeof(Instant), schema => - { - schema.Type = JsonObjectType.String; - schema.Format = JsonFormatStrings.DateTime; - }), - - new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String), - new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String) - }; - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs deleted file mode 100644 index a0fa2eb2f..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Authorization; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using Squidex.Web; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class ScopesProcessor : IOperationProcessor - { - public bool Process(OperationProcessorContext context) - { - if (context.OperationDescription.Operation.Security == null) - { - context.OperationDescription.Operation.Security = new List(); - } - - var permissionAttribute = context.MethodInfo.GetCustomAttribute(); - - if (permissionAttribute != null) - { - context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = permissionAttribute.PermissionIds - }); - } - else - { - var authorizeAttributes = - context.MethodInfo.GetCustomAttributes(true).Union( - context.MethodInfo.DeclaringType.GetCustomAttributes(true)) - .ToArray(); - - if (authorizeAttributes.Any()) - { - var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList(); - - context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = scopes - }); - } - } - - return true; - } - } -} diff --git a/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs deleted file mode 100644 index 3a9ef5d33..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Text.RegularExpressions; -using Namotion.Reflection; -using NSwag; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class XmlResponseTypesProcessor : IOperationProcessor - { - private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); - - public bool Process(OperationProcessorContext context) - { - var operation = context.OperationDescription.Operation; - - var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns"); - - if (!string.IsNullOrWhiteSpace(returnsDescription)) - { - foreach (Match match in ResponseRegex.Matches(returnsDescription)) - { - var statusCode = match.Groups["Code"].Value; - - if (!operation.Responses.TryGetValue(statusCode, out var response)) - { - response = new OpenApiResponse(); - - operation.Responses[statusCode] = response; - } - - var description = match.Groups["Description"].Value; - - if (description.Contains("=>")) - { - throw new InvalidOperationException("Description not formatted correcly."); - } - - response.Description = description; - } - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs deleted file mode 100644 index cb4a04043..000000000 --- a/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Mvc; -using Namotion.Reflection; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; - -namespace Squidex.Areas.Api.Config.OpenApi -{ - public sealed class XmlTagProcessor : IDocumentProcessor - { - public void Process(DocumentProcessorContext context) - { - foreach (var controllerType in context.ControllerTypes) - { - var attribute = controllerType.GetCustomAttribute(); - - if (attribute != null) - { - var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); - - if (tag != null) - { - var description = controllerType.GetXmlDocsSummary(); - - if (description != null) - { - tag.Description = tag.Description ?? string.Empty; - - if (!tag.Description.Contains(description)) - { - tag.Description += "\n\n" + description; - } - } - } - } - } - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs deleted file mode 100644 index 3a5a92dbf..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ /dev/null @@ -1,302 +0,0 @@ -// ========================================================================== -// 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.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using NSwag.Annotations; -using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Apps -{ - /// - /// Manages and configures apps. - /// - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppsController : ApiController - { - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IAppProvider appProvider; - private readonly IAppPlansProvider appPlansProvider; - - public AppsController(ICommandBus commandBus, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IAppProvider appProvider, - IAppPlansProvider appPlansProvider) - : base(commandBus) - { - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.appProvider = appProvider; - this.appPlansProvider = appPlansProvider; - } - - /// - /// Get your apps. - /// - /// - /// 200 => Apps returned. - /// - /// - /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). - /// You will retrieve all apps, where you are assigned as a contributor. - /// - [HttpGet] - [Route("apps/")] - [ProducesResponseType(typeof(AppDto[]), 200)] - [ApiPermission] - [ApiCosts(0)] - public async Task GetApps() - { - var userOrClientId = HttpContext.User.UserOrClientId(); - var userPermissions = HttpContext.Permissions(); - - var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); - - var response = Deferred.Response(() => - { - return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray(); - }); - - Response.Headers[HeaderNames.ETag] = apps.ToEtag(); - - return Ok(response); - } - - /// - /// Create a new app. - /// - /// The app object that needs to be added to squidex. - /// - /// 201 => App created. - /// 400 => App request not valid. - /// 409 => App name is already in use. - /// - /// - /// You can only create an app when you are authenticated as a user (OpenID implicit flow). - /// You will be assigned as owner of the new app automatically. - /// - [HttpPost] - [Route("apps/")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission] - [ApiCosts(0)] - public async Task PostApp([FromBody] CreateAppDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); - - return CreatedAtAction(nameof(GetApps), response); - } - - /// - /// Update the app. - /// - /// The name of the app to update. - /// The values to update. - /// - /// 200 => App updated. - /// 404 => App not found. - /// - [HttpPut] - [Route("apps/{app}/")] - [ProducesResponseType(typeof(AppDto), 200)] - [ApiPermission(Permissions.AppUpdateGeneral)] - [ApiCosts(0)] - public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); - - return Ok(response); - } - - /// - /// Get the app image. - /// - /// The name of the app to update. - /// The file to upload. - /// - /// 200 => App image uploaded. - /// 404 => App not found. - /// - [HttpPost] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission(Permissions.AppUpdateImage)] - [ApiCosts(0)] - public async Task UploadImage(string app, [OpenApiIgnore] List file) - { - var response = await InvokeCommandAsync(CreateCommand(file)); - - return Ok(response); - } - - /// - /// Get the app image. - /// - /// The name of the app. - /// - /// 200 => App image found and content or (resized) image returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(FileResult), 200)] - [AllowAnonymous] - [ApiCosts(0)] - public IActionResult GetImage(string app) - { - if (App.Image == null) - { - return NotFound(); - } - - var etag = App.Image.Etag; - - Response.Headers[HeaderNames.ETag] = etag; - - var handler = new Func(async bodyStream => - { - var assetId = App.Id.ToString(); - var assetResizedId = $"{assetId}_{etag}_Resized"; - - try - { - await assetStore.DownloadAsync(assetResizedId, bodyStream); - } - catch (AssetNotFoundException) - { - using (Profiler.Trace("Resize")) - { - using (var sourceStream = GetTempStream()) - { - using (var destinationStream = GetTempStream()) - { - using (Profiler.Trace("ResizeDownload")) - { - await assetStore.DownloadAsync(assetId, sourceStream); - sourceStream.Position = 0; - } - - using (Profiler.Trace("ResizeImage")) - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, 150, 150, "Crop"); - destinationStream.Position = 0; - } - - using (Profiler.Trace("ResizeUpload")) - { - await assetStore.UploadAsync(assetResizedId, destinationStream); - destinationStream.Position = 0; - } - - await destinationStream.CopyToAsync(bodyStream); - } - } - } - } - }); - - return new FileCallbackResult(App.Image.MimeType, null, true, handler); - } - - /// - /// Remove the app image. - /// - /// The name of the app to update. - /// - /// 200 => App image removed. - /// 404 => App not found. - /// - [HttpDelete] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission(Permissions.AppUpdate)] - [ApiCosts(0)] - public async Task DeleteImage(string app) - { - var response = await InvokeCommandAsync(new RemoveAppImage()); - - return Ok(response); - } - - /// - /// Archive the app. - /// - /// The name of the app to archive. - /// - /// 204 => App archived. - /// 404 => App not found. - /// - [HttpDelete] - [Route("apps/{app}/")] - [ApiPermission(Permissions.AppDelete)] - [ApiCosts(0)] - public async Task DeleteApp(string app) - { - await CommandBus.PublishAsync(new ArchiveApp()); - - return NoContent(); - } - - private async Task InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - var userOrClientId = HttpContext.User.UserOrClientId(); - var userPermissions = HttpContext.Permissions(); - - var result = context.Result(); - var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this); - - return response; - } - - private static UploadAppImage CreateCommand(IReadOnlyList file) - { - if (file.Count != 1) - { - var error = new ValidationError($"Can only upload one file, found {file.Count} files."); - - throw new ValidationException("Cannot create asset.", error); - } - - return new UploadAppImage { File = file[0].ToAssetFile() }; - } - - private static FileStream GetTempStream() - { - var tempFileName = Path.GetTempFileName(); - - return new FileStream(tempFileName, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, 1024 * 16, - FileOptions.Asynchronous | - FileOptions.DeleteOnClose | - FileOptions.SequentialScan); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs deleted file mode 100644 index 526b9c631..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ /dev/null @@ -1,244 +0,0 @@ -// ========================================================================== -// 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.ComponentModel.DataAnnotations; -using NodaTime; -using Squidex.Areas.Api.Controllers.Assets; -using Squidex.Areas.Api.Controllers.Backups; -using Squidex.Areas.Api.Controllers.Ping; -using Squidex.Areas.Api.Controllers.Plans; -using Squidex.Areas.Api.Controllers.Rules; -using Squidex.Areas.Api.Controllers.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Web; -using AllPermissions = Squidex.Shared.Permissions; - -#pragma warning disable RECS0033 // Convert 'if' to '||' expression - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class AppDto : Resource - { - /// - /// The name of the app. - /// - [Required] - [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } - - /// - /// The optional label of the app. - /// - public string Label { get; set; } - - /// - /// The optional description of the app. - /// - public string Description { get; set; } - - /// - /// The version of the app. - /// - public long Version { get; set; } - - /// - /// The id of the app. - /// - public Guid Id { get; set; } - - /// - /// The timestamp when the app has been created. - /// - public Instant Created { get; set; } - - /// - /// The timestamp when the app has been modified last. - /// - public Instant LastModified { get; set; } - - /// - /// The permission level of the user. - /// - public IEnumerable Permissions { get; set; } - - /// - /// Indicates if the user can access the api. - /// - public bool CanAccessApi { get; set; } - - /// - /// Indicates if the user can access at least one content. - /// - public bool CanAccessContent { get; set; } - - /// - /// Gets the current plan name. - /// - public string PlanName { get; set; } - - /// - /// Gets the next plan name. - /// - public string PlanUpgrade { get; set; } - - public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) - { - var permissions = GetPermissions(app, userId, userPermissions); - - var result = SimpleMapper.Map(app, new AppDto()); - - result.Permissions = permissions.ToIds(); - - if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppApi, app.Name), permissions)) - { - result.CanAccessApi = true; - } - - if (controller.Includes(AllPermissions.ForApp(AllPermissions.AppContents, app.Name), permissions)) - { - result.CanAccessContent = true; - } - - result.SetPlan(app, plans, controller, permissions); - result.SetImage(app, controller); - - return result.CreateLinks(controller, permissions); - } - - private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions) - { - var permissions = new List(); - - if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) - { - permissions.AddRange(role.Permissions); - } - - if (userPermissions != null) - { - permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); - } - - return new PermissionSet(permissions); - } - - private void SetPlan(IAppEntity app, IAppPlansProvider plans, ApiController controller, PermissionSet permissions) - { - if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name, additional: permissions)) - { - PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; - } - - PlanName = plans.GetPlanForApp(app)?.Name; - } - - private void SetImage(IAppEntity app, ApiController controller) - { - if (app.Image != null) - { - AddGetLink("image", controller.Url(x => nameof(x.GetImage), new { app = app.Name })); - } - } - - private AppDto CreateLinks(ApiController controller, PermissionSet permissions) - { - var values = new { app = Name }; - - AddGetLink("ping", controller.Url(x => nameof(x.GetAppPing), values)); - - if (controller.HasPermission(AllPermissions.AppDelete, Name, additional: permissions)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteApp), values)); - } - - if (controller.HasPermission(AllPermissions.AppUpdateGeneral, Name, additional: permissions)) - { - AddPutLink("update", controller.Url(x => nameof(x.UpdateApp), values)); - } - - if (controller.HasPermission(AllPermissions.AppUpdateImage, Name, additional: permissions)) - { - AddPostLink("image/upload", controller.Url(x => nameof(x.UploadImage), values)); - - AddDeleteLink("image/delete", controller.Url(x => nameof(x.DeleteImage), values)); - } - - if (controller.HasPermission(AllPermissions.AppAssetsRead, Name, additional: permissions)) - { - AddGetLink("assets", controller.Url(x => nameof(x.GetAssets), values)); - } - - if (controller.HasPermission(AllPermissions.AppBackupsRead, Name, additional: permissions)) - { - AddGetLink("backups", controller.Url(x => nameof(x.GetBackups), values)); - } - - if (controller.HasPermission(AllPermissions.AppClientsRead, Name, additional: permissions)) - { - AddGetLink("clients", controller.Url(x => nameof(x.GetClients), values)); - } - - if (controller.HasPermission(AllPermissions.AppContributorsRead, Name, additional: permissions)) - { - AddGetLink("contributors", controller.Url(x => nameof(x.GetContributors), values)); - } - - if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) - { - AddGetLink("languages", controller.Url(x => nameof(x.GetLanguages), values)); - } - - if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) - { - AddGetLink("patterns", controller.Url(x => nameof(x.GetPatterns), values)); - } - - if (controller.HasPermission(AllPermissions.AppPlansRead, Name, additional: permissions)) - { - AddGetLink("plans", controller.Url(x => nameof(x.GetPlans), values)); - } - - if (controller.HasPermission(AllPermissions.AppRolesRead, Name, additional: permissions)) - { - AddGetLink("roles", controller.Url(x => nameof(x.GetRoles), values)); - } - - if (controller.HasPermission(AllPermissions.AppRulesRead, Name, additional: permissions)) - { - AddGetLink("rules", controller.Url(x => nameof(x.GetRules), values)); - } - - if (controller.HasPermission(AllPermissions.AppCommon, Name, additional: permissions)) - { - AddGetLink("schemas", controller.Url(x => nameof(x.GetSchemas), values)); - } - - if (controller.HasPermission(AllPermissions.AppWorkflowsRead, Name, additional: permissions)) - { - AddGetLink("workflows", controller.Url(x => nameof(x.GetWorkflows), values)); - } - - if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, additional: permissions)) - { - AddPostLink("schemas/create", controller.Url(x => nameof(x.PostSchema), values)); - } - - if (controller.HasPermission(AllPermissions.AppAssetsCreate, Name, additional: permissions)) - { - AddPostLink("assets/create", controller.Url(x => nameof(x.PostSchema), values)); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs deleted file mode 100644 index 9efa1133a..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Squidex.Shared; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class ContributorDto : Resource - { - private const string NotFound = "- not found -"; - - /// - /// The id of the user that contributes to the app. - /// - [Required] - public string ContributorId { get; set; } - - /// - /// The display name. - /// - [Required] - public string ContributorName { get; set; } - - /// - /// The role of the contributor. - /// - public string Role { get; set; } - - public static ContributorDto FromIdAndRole(string id, string role) - { - var result = new ContributorDto { ContributorId = id, Role = role }; - - return result; - } - - public ContributorDto WithUser(IDictionary users) - { - if (users.TryGetValue(ContributorId, out var user)) - { - ContributorName = user.DisplayName(); - } - else - { - ContributorName = NotFound; - } - - return this; - } - - public ContributorDto WithLinks(ApiController controller, string app) - { - if (!controller.IsUser(ContributorId)) - { - if (controller.HasPermission(Permissions.AppContributorsAssign, app)) - { - AddPostLink("update", controller.Url(x => nameof(x.PostContributor), new { app })); - } - - if (controller.HasPermission(Permissions.AppContributorsRevoke, app)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContributor), new { app, id = ContributorId })); - } - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs deleted file mode 100644 index e22d45589..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class UpdateWorkflowDto - { - /// - /// The name of the workflow. - /// - public string Name { get; set; } - - /// - /// The workflow steps. - /// - [Required] - public Dictionary Steps { get; set; } - - /// - /// The schema ids. - /// - public List SchemaIds { get; set; } - - /// - /// The initial step. - /// - public Status Initial { get; set; } - - public UpdateWorkflow ToCommand(Guid id) - { - var workflow = new Workflow( - Initial, - Steps?.ToDictionary( - x => x.Key, - x => x.Value?.ToStep()), - SchemaIds, - Name); - - return new UpdateWorkflow { WorkflowId = id, Workflow = workflow }; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs deleted file mode 100644 index 85a41e991..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class WorkflowDto : Resource - { - /// - /// The workflow id. - /// - public Guid Id { get; set; } - - /// - /// The name of the workflow. - /// - public string Name { get; set; } - - /// - /// The workflow steps. - /// - [Required] - public Dictionary Steps { get; set; } - - /// - /// The schema ids. - /// - public IReadOnlyList SchemaIds { get; set; } - - /// - /// The initial step. - /// - public Status Initial { get; set; } - - public static WorkflowDto FromWorkflow(Guid id, Workflow workflow) - { - var result = SimpleMapper.Map(workflow, new WorkflowDto - { - Steps = workflow.Steps.ToDictionary( - x => x.Key, - x => WorkflowStepDto.FromWorkflowStep(x.Value)), - Id = id - }); - - return result; - } - - public WorkflowDto WithLinks(ApiController controller, string app) - { - var values = new { app, id = Id }; - - if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) - { - AddPutLink("update", controller.Url(x => nameof(x.PutWorkflow), values)); - } - - if (controller.HasPermission(Permissions.AppWorkflowsDelete, app)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteWorkflow), values)); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs deleted file mode 100644 index 3007c454b..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class WorkflowStepDto - { - /// - /// The transitions. - /// - [Required] - public Dictionary Transitions { get; set; } - - /// - /// The optional color. - /// - public string Color { get; set; } - - /// - /// Indicates if updates should not be allowed. - /// - public bool NoUpdate { get; set; } - - public static WorkflowStepDto FromWorkflowStep(WorkflowStep step) - { - if (step == null) - { - return null; - } - - return SimpleMapper.Map(step, new WorkflowStepDto - { - Transitions = step.Transitions.ToDictionary( - y => y.Key, - y => WorkflowTransitionDto.FromWorkflowTransition(y.Value)) - }); - } - - public WorkflowStep ToStep() - { - return new WorkflowStep( - Transitions?.ToDictionary( - y => y.Key, - y => y.Value?.ToTransition()), - Color, NoUpdate); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs deleted file mode 100644 index 9b7d221f9..000000000 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.ObjectModel; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class WorkflowTransitionDto - { - /// - /// The optional expression. - /// - public string Expression { get; set; } - - /// - /// The optional restricted role. - /// - public ReadOnlyCollection Roles { get; set; } - - public static WorkflowTransitionDto FromWorkflowTransition(WorkflowTransition transition) - { - if (transition == null) - { - return null; - } - - return new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles }; - } - - public WorkflowTransition ToTransition() - { - return new WorkflowTransition(Expression, Roles); - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs deleted file mode 100644 index dcfcd5491..000000000 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ /dev/null @@ -1,202 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Assets.Models; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Web; - -#pragma warning disable 1573 - -namespace Squidex.Areas.Api.Controllers.Assets -{ - /// - /// Uploads and retrieves assets. - /// - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetContentController : ApiController - { - private readonly IAssetStore assetStore; - private readonly IAssetRepository assetRepository; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - - public AssetContentController( - ICommandBus commandBus, - IAssetStore assetStore, - IAssetRepository assetRepository, - IAssetThumbnailGenerator assetThumbnailGenerator) - : base(commandBus) - { - this.assetStore = assetStore; - this.assetRepository = assetRepository; - this.assetThumbnailGenerator = assetThumbnailGenerator; - } - - /// - /// Get the asset content. - /// - /// The name of the app. - /// The id or slug of the asset. - /// Optional suffix that can be used to seo-optimize the link to the image Has not effect. - /// The query string parameters. - /// - /// 200 => Asset found and content or (resized) image returned. - /// 404 => Asset or app not found. - /// - [HttpGet] - [Route("assets/{app}/{idOrSlug}/{*more}")] - [ProducesResponseType(typeof(FileResult), 200)] - [ApiCosts(0.5)] - [AllowAnonymous] - public async Task GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetQuery query) - { - IAssetEntity asset; - - if (Guid.TryParse(idOrSlug, out var guid)) - { - asset = await assetRepository.FindAssetAsync(guid); - } - else - { - asset = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug); - } - - return DeliverAsset(asset, query); - } - - /// - /// Get the asset content. - /// - /// The id of the asset. - /// The query string parameters. - /// - /// 200 => Asset found and content or (resized) image returned. - /// 404 => Asset or app not found. - /// - [HttpGet] - [Route("assets/{id}/")] - [ProducesResponseType(typeof(FileResult), 200)] - [ApiCosts(0.5)] - public async Task GetAssetContent(Guid id, [FromQuery] AssetQuery query) - { - var asset = await assetRepository.FindAssetAsync(id); - - return DeliverAsset(asset, query); - } - - private IActionResult DeliverAsset(IAssetEntity asset, AssetQuery query) - { - query = query ?? new AssetQuery(); - - if (asset == null || asset.FileVersion < query.Version) - { - return NotFound(); - } - - var fileVersion = query.Version; - - if (fileVersion <= EtagVersion.Any) - { - fileVersion = asset.FileVersion; - } - - Response.Headers[HeaderNames.ETag] = fileVersion.ToString(); - - if (query.CacheDuration > 0) - { - Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}"; - } - - var handler = new Func(async bodyStream => - { - var assetId = asset.Id.ToString(); - - if (asset.IsImage && query.ShouldResize()) - { - var assetSuffix = $"{query.Width}_{query.Height}_{query.Mode}"; - - if (query.Quality.HasValue) - { - assetSuffix += $"_{query.Quality}"; - } - - try - { - await assetStore.DownloadAsync(assetId, fileVersion, assetSuffix, bodyStream); - } - catch (AssetNotFoundException) - { - using (Profiler.Trace("Resize")) - { - using (var sourceStream = GetTempStream()) - { - using (var destinationStream = GetTempStream()) - { - using (Profiler.Trace("ResizeDownload")) - { - await assetStore.DownloadAsync(assetId, fileVersion, null, sourceStream); - sourceStream.Position = 0; - } - - using (Profiler.Trace("ResizeImage")) - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, query.Width, query.Height, query.Mode, query.Quality); - destinationStream.Position = 0; - } - - using (Profiler.Trace("ResizeUpload")) - { - await assetStore.UploadAsync(assetId, fileVersion, assetSuffix, destinationStream); - destinationStream.Position = 0; - } - - await destinationStream.CopyToAsync(bodyStream); - } - } - } - } - } - else - { - await assetStore.DownloadAsync(assetId, fileVersion, null, bodyStream); - } - }); - - if (query.Download == 1) - { - return new FileCallbackResult(asset.MimeType, asset.FileName, true, handler); - } - else - { - return new FileCallbackResult(asset.MimeType, null, true, handler); - } - } - - private static FileStream GetTempStream() - { - var tempFileName = Path.GetTempFileName(); - - return new FileStream(tempFileName, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, 1024 * 16, - FileOptions.Asynchronous | - FileOptions.DeleteOnClose | - FileOptions.SequentialScan); - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs deleted file mode 100644 index 394c91a5b..000000000 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ /dev/null @@ -1,319 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using NSwag.Annotations; -using Squidex.Areas.Api.Controllers.Assets.Models; -using Squidex.Areas.Api.Controllers.Contents; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Assets -{ - /// - /// Uploads and retrieves assets. - /// - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetsController : ApiController - { - private readonly IAssetQueryService assetQuery; - private readonly IAssetUsageTracker assetStatsRepository; - private readonly IAppPlansProvider appPlansProvider; - private readonly MyContentsControllerOptions controllerOptions; - private readonly ITagService tagService; - private readonly AssetOptions assetOptions; - - public AssetsController( - ICommandBus commandBus, - IAssetQueryService assetQuery, - IAssetUsageTracker assetStatsRepository, - IAppPlansProvider appPlansProvider, - IOptions assetOptions, - IOptions controllerOptions, - ITagService tagService) - : base(commandBus) - { - this.assetOptions = assetOptions.Value; - this.assetQuery = assetQuery; - this.assetStatsRepository = assetStatsRepository; - this.appPlansProvider = appPlansProvider; - this.controllerOptions = controllerOptions.Value; - this.tagService = tagService; - } - - /// - /// Get assets tags. - /// - /// The name of the app. - /// - /// 200 => Assets returned. - /// 404 => App not found. - /// - /// - /// Get all tags for assets. - /// - [HttpGet] - [Route("apps/{app}/assets/tags")] - [ProducesResponseType(typeof(Dictionary), 200)] - [ApiPermission(Permissions.AppAssetsRead)] - [ApiCosts(1)] - public async Task GetTags(string app) - { - var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); - - Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); - - return Ok(tags); - } - - /// - /// Get assets. - /// - /// The name of the app. - /// The optional asset ids. - /// The optional json query. - /// - /// 200 => Assets returned. - /// 404 => App not found. - /// - /// - /// Get all assets for the app. - /// - [HttpGet] - [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] - [ApiCosts(1)] - public async Task GetAssets(string app, [FromQuery] string ids = null, [FromQuery] string q = null) - { - var assets = await assetQuery.QueryAsync(Context, - Q.Empty - .WithIds(ids) - .WithJsonQuery(q) - .WithODataQuery(Request.QueryString.ToString())); - - var response = Deferred.Response(() => - { - return AssetsDto.FromAssets(assets, this, app); - }); - - if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); - } - - Response.Headers[HeaderNames.ETag] = assets.ToEtag(); - - return Ok(response); - } - - /// - /// Get an asset by id. - /// - /// The name of the app. - /// The id of the asset to retrieve. - /// - /// 200 => Asset found. - /// 404 => Asset or app not found. - /// - [HttpGet] - [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] - [ApiCosts(1)] - public async Task GetAsset(string app, Guid id) - { - var asset = await assetQuery.FindAssetAsync(Context, id); - - if (asset == null) - { - return NotFound(); - } - - var response = Deferred.Response(() => - { - return AssetDto.FromAsset(asset, this, app); - }); - - if (controllerOptions.EnableSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); - } - - Response.Headers[HeaderNames.ETag] = asset.ToEtag(); - - return Ok(response); - } - - /// - /// Upload a new asset. - /// - /// The name of the app. - /// The file to upload. - /// - /// 201 => Asset created. - /// 404 => App not found. - /// 400 => Asset exceeds the maximum size. - /// - /// - /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. - /// - [HttpPost] - [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetDto), 201)] - [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsCreate)] - [ApiCosts(1)] - public async Task PostAsset(string app, [OpenApiIgnore] List file) - { - var assetFile = await CheckAssetFileAsync(file); - - var command = new CreateAsset { File = assetFile }; - - var response = await InvokeCommandAsync(app, command); - - return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); - } - - /// - /// Replace asset content. - /// - /// The name of the app. - /// The id of the asset. - /// The file to upload. - /// - /// 200 => Asset updated. - /// 404 => Asset or app not found. - /// 400 => Asset exceeds the maximum size. - /// - /// - /// Use multipart request to upload an asset. - /// - [HttpPut] - [Route("apps/{app}/assets/{id}/content/")] - [ProducesResponseType(typeof(AssetDto), 200)] - [ApiPermission(Permissions.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task PutAssetContent(string app, Guid id, [OpenApiIgnore] List file) - { - var assetFile = await CheckAssetFileAsync(file); - - var command = new UpdateAsset { File = assetFile, AssetId = id }; - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Updates the asset. - /// - /// The name of the app. - /// The id of the asset. - /// The asset object that needs to updated. - /// - /// 200 => Asset updated. - /// 400 => Asset name not valid. - /// 404 => Asset or app not found. - /// - [HttpPut] - [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetDto), 200)] - [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) - { - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Delete an asset. - /// - /// The name of the app. - /// The id of the asset to delete. - /// - /// 204 => Asset deleted. - /// 404 => Asset or app not found. - /// - [HttpDelete] - [Route("apps/{app}/assets/{id}/")] - [ApiPermission(Permissions.AppAssetsDelete)] - [ApiCosts(1)] - public async Task DeleteAsset(string app, Guid id) - { - await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); - - return NoContent(); - } - - private async Task InvokeCommandAsync(string app, ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - if (context.PlainResult is AssetCreatedResult created) - { - return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); - } - else - { - return AssetDto.FromAsset(context.Result(), this, app); - } - } - - private async Task CheckAssetFileAsync(IReadOnlyList file) - { - if (file.Count != 1) - { - var error = new ValidationError($"Can only upload one file, found {file.Count} files."); - - throw new ValidationException("Cannot create asset.", error); - } - - var formFile = file[0]; - - if (formFile.Length > assetOptions.MaxSize) - { - var error = new ValidationError($"File cannot be bigger than {assetOptions.MaxSize.ToReadableSize()}."); - - throw new ValidationException("Cannot create asset.", error); - } - - var plan = appPlansProvider.GetPlanForApp(App); - - var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); - - if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length) - { - var error = new ValidationError("You have reached your max asset size."); - - throw new ValidationException("Cannot create asset.", error); - } - - return formFile.ToAssetFile(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs deleted file mode 100644 index f648c7050..000000000 --- a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Orleans; -using Squidex.Areas.Api.Controllers.Backups.Models; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Backups -{ - /// - /// Manages backups for apps. - /// - [ApiExplorerSettings(GroupName = nameof(Backups))] - public class RestoreController : ApiController - { - private readonly IGrainFactory grainFactory; - - public RestoreController(ICommandBus commandBus, IGrainFactory grainFactory) - : base(commandBus) - { - this.grainFactory = grainFactory; - } - - /// - /// Get current restore status. - /// - /// - /// 200 => Status returned. - /// - [HttpGet] - [Route("apps/restore/")] - [ProducesResponseType(typeof(RestoreJobDto), 200)] - [ApiPermission(Permissions.AdminRestore)] - public async Task GetRestoreJob() - { - var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); - - var job = await restoreGrain.GetJobAsync(); - - if (job.Value == null) - { - return NotFound(); - } - - var response = RestoreJobDto.FromJob(job.Value); - - return Ok(response); - } - - /// - /// Restore a backup. - /// - /// The backup to restore. - /// - /// 204 => Restore operation started. - /// - [HttpPost] - [Route("apps/restore/")] - [ApiPermission(Permissions.AdminRestore)] - public async Task PostRestoreJob([FromBody] RestoreRequestDto request) - { - var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); - - await restoreGrain.RestoreAsync(request.Url, User.Token(), request.Name); - - return NoContent(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs deleted file mode 100644 index 2f9b55f1d..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ /dev/null @@ -1,457 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Contents.Models; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Infrastructure.Commands; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Contents -{ - public sealed class ContentsController : ApiController - { - private readonly MyContentsControllerOptions controllerOptions; - private readonly IContentQueryService contentQuery; - private readonly IContentWorkflow contentWorkflow; - private readonly IGraphQLService graphQl; - - public ContentsController(ICommandBus commandBus, - IContentQueryService contentQuery, - IContentWorkflow contentWorkflow, - IGraphQLService graphQl, - IOptions controllerOptions) - : base(commandBus) - { - this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; - this.controllerOptions = controllerOptions.Value; - - this.graphQl = graphQl; - } - - /// - /// GraphQL endpoint. - /// - /// The name of the app. - /// The graphql query. - /// - /// 200 => Contents retrieved or mutated. - /// 404 => Schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [HttpPost] - [Route("content/{app}/graphql/")] - [ApiPermission] - [ApiCosts(2)] - public async Task PostGraphQL(string app, [FromBody] GraphQLQuery query) - { - var (hasError, response) = await graphQl.QueryAsync(Context, query); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } - } - - /// - /// GraphQL endpoint (Batch). - /// - /// The name of the app. - /// The graphql queries. - /// - /// 200 => Contents retrieved or mutated. - /// 404 => Schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [HttpPost] - [Route("content/{app}/graphql/batch")] - [ApiPermission] - [ApiCosts(2)] - public async Task PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch) - { - var (hasError, response) = await graphQl.QueryAsync(Context, batch); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } - } - - /// - /// Queries contents. - /// - /// The name of the app. - /// The optional ids of the content to fetch. - /// - /// 200 => Contents retrieved. - /// 404 => App not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task GetAllContents(string app, [FromQuery] string ids) - { - var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); - - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); - }); - - if (ShouldProvideSurrogateKeys(contents)) - { - Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); - } - - Response.Headers[HeaderNames.ETag] = contents.ToEtag(); - - return Ok(response); - } - - /// - /// Queries contents. - /// - /// The name of the app. - /// The name of the schema. - /// The optional ids of the content to fetch. - /// The optional json query. - /// - /// 200 => Contents retrieved. - /// 404 => Schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/{name}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] string q = null) - { - var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var contents = await contentQuery.QueryAsync(Context, name, - Q.Empty - .WithIds(ids) - .WithJsonQuery(q) - .WithODataQuery(Request.QueryString.ToString())); - - var response = Deferred.AsyncResponse(async () => - { - return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); - }); - - if (ShouldProvideSurrogateKeys(contents)) - { - Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); - } - - Response.Headers[HeaderNames.ETag] = contents.ToEtag(); - - return Ok(response); - } - - /// - /// Get a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content to fetch. - /// - /// 200 => Content found. - /// 404 => Content, schema or app not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task GetContent(string app, string name, Guid id) - { - var content = await contentQuery.FindContentAsync(Context, name, id); - - var response = ContentDto.FromContent(Context, content, this); - - if (controllerOptions.EnableSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); - } - - Response.Headers[HeaderNames.ETag] = content.ToEtag(); - - return Ok(response); - } - - /// - /// Get a content by version. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content to fetch. - /// The version fo the content to fetch. - /// - /// 200 => Content found. - /// 404 => Content, schema or app not found. - /// 400 => Content data is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpGet] - [Route("content/{app}/{name}/{id}/{version}/")] - [ApiPermission(Permissions.AppContentsRead)] - [ApiCosts(1)] - public async Task GetContentVersion(string app, string name, Guid id, int version) - { - var content = await contentQuery.FindContentAsync(Context, name, id, version); - - var response = ContentDto.FromContent(Context, content, this); - - if (controllerOptions.EnableSurrogateKeys) - { - Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); - } - - Response.Headers[HeaderNames.ETag] = content.ToEtag(); - - return Ok(response.Data); - } - - /// - /// Create a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The full data for the content item. - /// Indicates whether the content should be published immediately. - /// - /// 201 => Content created. - /// 404 => Content, schema or app not found. - /// 400 => Content data is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPost] - [Route("content/{app}/{name}/")] - [ProducesResponseType(typeof(ContentsDto), 201)] - [ApiPermission(Permissions.AppContentsCreate)] - [ApiCosts(1)] - public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; - - var response = await InvokeCommandAsync(command); - - return CreatedAtAction(nameof(GetContent), new { app, id = command.ContentId }, response); - } - - /// - /// Update a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to update. - /// The full data for the content item. - /// Indicates whether the update is a proposal. - /// - /// 200 => Content updated. - /// 404 => Content, schema or app not found. - /// 400 => Content data is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPut] - [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] - [ApiCosts(1)] - public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Patchs a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to patch. - /// The patch for the content item. - /// Indicates whether the patch is a proposal. - /// - /// 200 => Content patched. - /// 404 => Content, schema or app not found. - /// 400 => Content patch is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPatch] - [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] - [ApiCosts(1)] - public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Publish a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to publish. - /// The status request. - /// - /// 200 => Content published. - /// 404 => Content, schema or app not found. - /// 400 => Request is not valid. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPut] - [Route("content/{app}/{name}/{id}/status/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission] - [ApiCosts(1)] - public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Discard changes. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to discard changes. - /// - /// 200 => Content restored. - /// 404 => Content, schema or app not found. - /// 400 => Content was not archived. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPut] - [Route("content/{app}/{name}/{id}/discard/")] - [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsDraftDiscard)] - [ApiCosts(1)] - public async Task DiscardDraft(string app, string name, Guid id) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new DiscardChanges { ContentId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// - /// Delete a content item. - /// - /// The name of the app. - /// The name of the schema. - /// The id of the content item to delete. - /// - /// 204 => Content deleted. - /// 404 => Content, schema or app not found. - /// - /// - /// You can create an generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpDelete] - [Route("content/{app}/{name}/{id}/")] - [ApiPermission(Permissions.AppContentsDelete)] - [ApiCosts(1)] - public async Task DeleteContent(string app, string name, Guid id) - { - await contentQuery.GetSchemaOrThrowAsync(Context, name); - - var command = new DeleteContent { ContentId = id }; - - await CommandBus.PublishAsync(command); - - return NoContent(); - } - - private async Task InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - var result = context.Result(); - var response = ContentDto.FromContent(Context, result, this); - - return response; - } - - private bool ShouldProvideSurrogateKeys(IReadOnlyList response) - { - return controllerOptions.EnableSurrogateKeys && response.Count <= controllerOptions.MaxItemsForSurrogateKeys; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs deleted file mode 100644 index 8399e0edd..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ /dev/null @@ -1,194 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using NodaTime; -using Squidex.Areas.Api.Controllers.Schemas.Models; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public sealed class ContentDto : Resource - { - /// - /// The if of the content item. - /// - public Guid Id { get; set; } - - /// - /// The user that has created the content item. - /// - [Required] - public RefToken CreatedBy { get; set; } - - /// - /// The user that has updated the content item. - /// - [Required] - public RefToken LastModifiedBy { get; set; } - - /// - /// The data of the content item. - /// - [Required] - public object Data { get; set; } - - /// - /// The pending changes of the content item. - /// - public object DataDraft { get; set; } - - /// - /// The reference data for the frontend UI. - /// - public NamedContentData ReferenceData { get; set; } - - /// - /// Indicates if the draft data is pending. - /// - public bool IsPending { get; set; } - - /// - /// The scheduled status. - /// - public ScheduleJobDto ScheduleJob { get; set; } - - /// - /// The date and time when the content item has been created. - /// - public Instant Created { get; set; } - - /// - /// The date and time when the content item has been modified last. - /// - public Instant LastModified { get; set; } - - /// - /// The status of the content. - /// - public Status Status { get; set; } - - /// - /// The color of the status. - /// - public string StatusColor { get; set; } - - /// - /// The name of the schema. - /// - public string SchemaName { get; set; } - - /// - /// The display name of the schema. - /// - public string SchemaDisplayName { get; set; } - - /// - /// The reference fields. - /// - public FieldDto[] ReferenceFields { get; set; } - - /// - /// The version of the content. - /// - public long Version { get; set; } - - public static ContentDto FromContent(Context context, IEnrichedContentEntity content, ApiController controller) - { - var response = SimpleMapper.Map(content, new ContentDto()); - - if (context.IsFlatten()) - { - response.Data = content.Data?.ToFlatten(); - response.DataDraft = content.DataDraft?.ToFlatten(); - } - else - { - response.Data = content.Data; - response.DataDraft = content.DataDraft; - } - - if (content.ReferenceFields != null) - { - response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray(); - } - - if (content.ScheduleJob != null) - { - response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); - } - - return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name); - } - - private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema) - { - var values = new { app, name = schema, id = Id }; - - AddSelfLink(controller.Url(x => nameof(x.GetContent), values)); - - if (Version > 0) - { - var versioned = new { app, name = schema, id = Id, version = Version - 1 }; - - AddGetLink("prev", controller.Url(x => nameof(x.GetContentVersion), versioned)); - } - - if (IsPending) - { - if (controller.HasPermission(Permissions.AppContentsDraftDiscard, app, schema)) - { - AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); - } - - if (controller.HasPermission(Permissions.AppContentsDraftPublish, app, schema)) - { - AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); - } - } - - if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) - { - if (content.CanUpdate) - { - AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); - } - - if (Status == Status.Published) - { - AddPutLink("draft/propose", controller.Url((ContentsController x) => nameof(x.PutContent), values) + "?asDraft=true"); - } - - AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); - - if (content.Nexts != null) - { - foreach (var next in content.Nexts) - { - AddPutLink($"status/{next.Status}", controller.Url(x => nameof(x.PutContentStatus), values), next.Color); - } - } - } - - if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs deleted file mode 100644 index cd02ddaa7..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public sealed class ContentsDto : Resource - { - /// - /// The total number of content items. - /// - public long Total { get; set; } - - /// - /// The content items. - /// - [Required] - public ContentDto[] Items { get; set; } - - /// - /// The possible statuses. - /// - [Required] - public StatusInfoDto[] Statuses { get; set; } - - public static async Task FromContentsAsync(IResultList contents, Context context, ApiController controller, - ISchemaEntity schema, IContentWorkflow workflow) - { - var result = new ContentsDto - { - Total = contents.Total, - Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() - }; - - if (schema != null) - { - await result.AssignStatusesAsync(workflow, schema); - - result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); - } - - return result; - } - - private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) - { - var allStatuses = await workflow.GetAllAsync(schema); - - Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); - } - - private ContentsDto CreateLinks(ApiController controller, string app, string schema) - { - var values = new { app, name = schema }; - - AddSelfLink(controller.Url(x => nameof(x.GetContents), values)); - - if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) - { - AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); - - AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); - } - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs deleted file mode 100644 index 99de9745b..000000000 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Plans.Models; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.Commands; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Plans -{ - /// - /// Manages and configures plans. - /// - [ApiExplorerSettings(GroupName = nameof(Plans))] - public sealed class AppPlansController : ApiController - { - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; - - public AppPlansController(ICommandBus commandBus, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager) - : base(commandBus) - { - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; - } - - /// - /// Get app plan information. - /// - /// The name of the app. - /// - /// 200 => App plan information returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/plans/")] - [ProducesResponseType(typeof(AppPlansDto), 200)] - [ApiPermission(Permissions.AppPlansRead)] - [ApiCosts(0)] - public IActionResult GetPlans(string app) - { - var hasPortal = appPlansBillingManager.HasPortal; - - var response = Deferred.Response(() => - { - return AppPlansDto.FromApp(App, appPlansProvider, hasPortal); - }); - - Response.Headers[HeaderNames.ETag] = App.ToEtag(); - - return Ok(response); - } - - /// - /// Change the app plan. - /// - /// The name of the app. - /// Plan object that needs to be changed. - /// - /// 200 => Plan changed or redirect url returned. - /// 400 => Plan not owned by user. - /// 404 => App not found. - /// - [HttpPut] - [Route("apps/{app}/plan/")] - [ProducesResponseType(typeof(PlanChangedDto), 200)] - [ApiPermission(Permissions.AppPlansChange)] - [ApiCosts(0)] - public async Task PutPlan(string app, [FromBody] ChangePlanDto request) - { - var context = await CommandBus.PublishAsync(request.ToCommand()); - - string redirectUri = null; - - if (context.PlainResult is RedirectToCheckoutResult result) - { - redirectUri = result.Url.ToString(); - } - - return Ok(new PlanChangedDto { RedirectUri = redirectUri }); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs deleted file mode 100644 index 90d995e57..000000000 --- a/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; - -namespace Squidex.Areas.Api.Controllers.Plans.Models -{ - public sealed class AppPlansDto - { - /// - /// The available plans. - /// - [Required] - public PlanDto[] Plans { get; set; } - - /// - /// The current plan id. - /// - public string CurrentPlanId { get; set; } - - /// - /// The plan owner. - /// - public string PlanOwner { get; set; } - - /// - /// Indicates if there is a billing portal. - /// - public bool HasPortal { get; set; } - - public static AppPlansDto FromApp(IAppEntity app, IAppPlansProvider plans, bool hasPortal) - { - var planId = app.Plan?.PlanId; - - var response = new AppPlansDto - { - CurrentPlanId = planId, - Plans = plans.GetAvailablePlans().Select(PlanDto.FromPlan).ToArray(), - PlanOwner = app.Plan?.Owner.Identifier, - HasPortal = hasPortal - }; - - return response; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs deleted file mode 100644 index a35510786..000000000 --- a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Areas.Api.Controllers.Plans.Models -{ - public sealed class PlanChangedDto - { - /// - /// Optional redirect uri. - /// - public string RedirectUri { get; set; } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs deleted file mode 100644 index a39b15600..000000000 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Namotion.Reflection; -using NJsonSchema; -using NSwag.Generation.Processors; -using NSwag.Generation.Processors.Contexts; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -namespace Squidex.Areas.Api.Controllers.Rules.Models -{ - public sealed class RuleActionProcessor : IDocumentProcessor - { - private readonly RuleRegistry ruleRegistry; - - public RuleActionProcessor(RuleRegistry ruleRegistry) - { - Guard.NotNull(ruleRegistry, nameof(ruleRegistry)); - - this.ruleRegistry = ruleRegistry; - } - - public void Process(DocumentProcessorContext context) - { - try - { - var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false); - - if (schema != null) - { - schema.DiscriminatorObject = new OpenApiDiscriminator - { - JsonInheritanceConverter = new RuleActionConverter(), PropertyName = "actionType" - }; - - schema.Properties["actionType"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, IsRequired = true - }; - - foreach (var (key, value) in ruleRegistry.Actions) - { - var derivedSchema = context.SchemaGenerator.Generate(value.Type.ToContextualType(), context.SchemaResolver); - - var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; - - if (oldName != null) - { - context.Document.Definitions.Remove(oldName); - context.Document.Definitions.Add($"{key}RuleActionDto", derivedSchema); - } - } - - RemoveFreezable(context, schema); - } - } - catch (KeyNotFoundException) - { - return; - } - } - - private static void RemoveFreezable(DocumentProcessorContext context, JsonSchema schema) - { - context.Document.Definitions.Remove("Freezable"); - - schema.AllOf.Clear(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs deleted file mode 100644 index 6c7785e60..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Web.Json; - -namespace Squidex.Areas.Api.Controllers.Schemas.Models -{ - [JsonConverter(typeof(TypedJsonInheritanceConverter), "fieldType")] - [KnownType(nameof(Subtypes))] - public abstract class FieldPropertiesDto - { - /// - /// Optional label for the editor. - /// - [StringLength(100)] - public string Label { get; set; } - - /// - /// Hints to describe the schema. - /// - [StringLength(1000)] - public string Hints { get; set; } - - /// - /// Placeholder to show when no value has been entered. - /// - [StringLength(100)] - public string Placeholder { get; set; } - - /// - /// Indicates if the field is required. - /// - public bool IsRequired { get; set; } - - /// - /// Determines if the field should be displayed in lists. - /// - public bool IsListField { get; set; } - - /// - /// Determines if the field should be displayed in reference lists. - /// - public bool IsReferenceField { get; set; } - - /// - /// Optional url to the editor. - /// - public string EditorUrl { get; set; } - - /// - /// Tags for automation processes. - /// - public ReadOnlyCollection Tags { get; set; } - - public abstract FieldProperties ToProperties(); - - public static Type[] Subtypes() - { - var type = typeof(FieldPropertiesDto); - - return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs deleted file mode 100644 index 0a55fe8e9..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Entities.Schemas.Commands; - -namespace Squidex.Areas.Api.Controllers.Schemas.Models -{ - public sealed class UpdateFieldDto - { - /// - /// The field properties. - /// - [Required] - public FieldPropertiesDto Properties { get; set; } - - public UpdateField ToCommand(long id, long? parentId = null) - { - return new UpdateField { ParentFieldId = parentId, FieldId = id, Properties = Properties?.ToProperties() }; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs deleted file mode 100644 index 63ba803ec..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Schemas.Models -{ - public abstract class UpsertSchemaDto - { - /// - /// The optional properties. - /// - public SchemaPropertiesDto Properties { get; set; } - - /// - /// The optional scripts. - /// - public SchemaScriptsDto Scripts { get; set; } - - /// - /// Optional fields. - /// - public List Fields { get; set; } - - /// - /// The optional preview urls. - /// - public Dictionary PreviewUrls { get; set; } - - /// - /// The category. - /// - public string Category { get; set; } - - /// - /// Set it to true to autopublish the schema. - /// - public bool IsPublished { get; set; } - - public static TCommand ToCommand(TDto dto, TCommand command) where TCommand : UpsertCommand where TDto : UpsertSchemaDto - { - SimpleMapper.Map(dto, command); - - if (dto.Properties != null) - { - command.Properties = new SchemaProperties(); - - SimpleMapper.Map(dto.Properties, command.Properties); - } - - if (dto.Scripts != null) - { - command.Scripts = new SchemaScripts(); - - SimpleMapper.Map(dto.Scripts, command.Scripts); - } - - if (dto.Fields != null) - { - command.Fields = new List(); - - foreach (var rootFieldDto in dto.Fields) - { - var rootProps = rootFieldDto?.Properties?.ToProperties(); - var rootField = new UpsertSchemaField { Properties = rootProps }; - - SimpleMapper.Map(rootFieldDto, rootField); - - if (rootFieldDto?.Nested?.Count > 0) - { - rootField.Nested = new List(); - - foreach (var nestedFieldDto in rootFieldDto.Nested) - { - var nestedProps = nestedFieldDto?.Properties?.ToProperties(); - var nestedField = new UpsertSchemaNestedField { Properties = nestedProps }; - - SimpleMapper.Map(nestedFieldDto, nestedField); - - rootField.Nested.Add(nestedField); - } - } - - command.Fields.Add(rootField); - } - } - - return command; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs deleted file mode 100644 index ea82636b5..000000000 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ /dev/null @@ -1,330 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Schemas.Models; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure.Commands; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Schemas -{ - /// - /// Manages and retrieves information about schemas. - /// - [ApiExplorerSettings(GroupName = nameof(Schemas))] - public sealed class SchemasController : ApiController - { - private readonly IAppProvider appProvider; - - public SchemasController(ICommandBus commandBus, IAppProvider appProvider) - : base(commandBus) - { - this.appProvider = appProvider; - } - - /// - /// Get schemas. - /// - /// The name of the app. - /// - /// 200 => Schemas returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemasDto), 200)] - [ApiPermission(Permissions.AppCommon)] - [ApiCosts(0)] - public async Task GetSchemas(string app) - { - var schemas = await appProvider.GetSchemasAsync(AppId); - - var response = Deferred.Response(() => - { - return SchemasDto.FromSchemas(schemas, this, app); - }); - - Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); - - return Ok(response); - } - - /// - /// Get a schema by name. - /// - /// The name of the app. - /// The name of the schema to retrieve. - /// - /// 200 => Schema found. - /// 404 => Schema or app not found. - /// - [HttpGet] - [Route("apps/{app}/schemas/{name}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppCommon)] - [ApiCosts(0)] - public async Task GetSchema(string app, string name) - { - ISchemaEntity schema; - - if (Guid.TryParse(name, out var id)) - { - schema = await appProvider.GetSchemaAsync(AppId, id); - } - else - { - schema = await appProvider.GetSchemaAsync(AppId, name); - } - - if (schema == null || schema.IsDeleted) - { - return NotFound(); - } - - var response = Deferred.Response(() => - { - return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); - }); - - Response.Headers[HeaderNames.ETag] = schema.ToEtag(); - - return Ok(response); - } - - /// - /// Create a new schema. - /// - /// The name of the app. - /// The schema object that needs to be added to the app. - /// - /// 201 => Schema created. - /// 400 => Schema name or properties are not valid. - /// 409 => Schema name already in use. - /// - [HttpPost] - [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 201)] - [ApiPermission(Permissions.AppSchemasCreate)] - [ApiCosts(1)] - public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return CreatedAtAction(nameof(GetSchema), new { app, name = request.Name }, response); - } - - /// - /// Update a schema. - /// - /// The name of the app. - /// The name of the schema. - /// The schema object that needs to updated. - /// - /// 200 => Schema updated. - /// 400 => Schema properties are not valid. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Synchronize a schema. - /// - /// The name of the app. - /// The name of the schema. - /// The schema object that needs to updated. - /// - /// 200 => Schema updated. - /// 400 => Schema properties are not valid. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/sync")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Update a schema category. - /// - /// The name of the app. - /// The name of the schema. - /// The schema object that needs to updated. - /// - /// 200 => Schema updated. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/category")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Update the preview urls. - /// - /// The name of the app. - /// The name of the schema. - /// The preview urls for the schema. - /// - /// 200 => Schema updated. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/preview-urls")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Update the scripts. - /// - /// The name of the app. - /// The name of the schema. - /// The schema scripts object that needs to updated. - /// - /// 200 => Schema updated. - /// 400 => Schema properties are not valid. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/scripts/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasScripts)] - [ApiCosts(1)] - public async Task PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Publish a schema. - /// - /// The name of the app. - /// The name of the schema to publish. - /// - /// 200 => Schema has been published. - /// 400 => Schema is already published. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/publish/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasPublish)] - [ApiCosts(1)] - public async Task PublishSchema(string app, string name) - { - var command = new PublishSchema(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Unpublish a schema. - /// - /// The name of the app. - /// The name of the schema to unpublish. - /// - /// 200 => Schema has been unpublished. - /// 400 => Schema is not published. - /// 404 => Schema or app not found. - /// - [HttpPut] - [Route("apps/{app}/schemas/{name}/unpublish/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasPublish)] - [ApiCosts(1)] - public async Task UnpublishSchema(string app, string name) - { - var command = new UnpublishSchema(); - - var response = await InvokeCommandAsync(app, command); - - return Ok(response); - } - - /// - /// Delete a schema. - /// - /// The name of the app. - /// The name of the schema to delete. - /// - /// 204 => Schema deleted. - /// 404 => Schema or app not found. - /// - [HttpDelete] - [Route("apps/{app}/schemas/{name}/")] - [ApiPermission(Permissions.AppSchemasDelete)] - [ApiCosts(1)] - public async Task DeleteSchema(string app, string name) - { - await CommandBus.PublishAsync(new DeleteSchema()); - - return NoContent(); - } - - private async Task InvokeCommandAsync(string app, ICommand command) - { - var context = await CommandBus.PublishAsync(command); - - var result = context.Result(); - var response = SchemaDetailsDto.FromSchemaWithDetails(result, this, app); - - return response; - } - } -} \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs deleted file mode 100644 index 81260dd3f..000000000 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared.Users; -using Squidex.Web; -using AllPermissions = Squidex.Shared.Permissions; - -namespace Squidex.Areas.Api.Controllers.Users.Models -{ - public sealed class UserDto : Resource - { - /// - /// The id of the user. - /// - [Required] - public string Id { get; set; } - - /// - /// The email of the user. Unique value. - /// - [Required] - public string Email { get; set; } - - /// - /// The display name (usually first name and last name) of the user. - /// - [Required] - public string DisplayName { get; set; } - - /// - /// Determines if the user is locked. - /// - [Required] - public bool IsLocked { get; set; } - - /// - /// Additional permissions for the user. - /// - [Required] - public IEnumerable Permissions { get; set; } - - public static UserDto FromUser(IUser user, ApiController controller) - { - var userPermssions = user.Permissions().ToIds(); - var userName = user.DisplayName(); - - var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); - - return result.CreateLinks(controller); - } - - private UserDto CreateLinks(ApiController controller) - { - var values = new { id = Id }; - - if (controller is UserManagementController) - { - AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); - } - else - { - AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); - } - - if (!controller.IsUser(Id)) - { - if (controller.HasPermission(AllPermissions.AdminUsersLock) && !IsLocked) - { - AddPutLink("lock", controller.Url(c => nameof(c.LockUser), values)); - } - - if (controller.HasPermission(AllPermissions.AdminUsersUnlock) && IsLocked) - { - AddPutLink("unlock", controller.Url(c => nameof(c.UnlockUser), values)); - } - } - - if (controller.HasPermission(AllPermissions.AdminUsersUpdate)) - { - AddPutLink("update", controller.Url(c => nameof(c.PutUser), values)); - } - - AddGetLink("picture", controller.Url(c => nameof(c.GetUserPicture), values)); - - return this; - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs deleted file mode 100644 index 9167ef148..000000000 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Squidex.Areas.Api.Controllers.Users.Models; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; -using Squidex.Shared; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Users -{ - [ApiModelValidation(true)] - public sealed class UserManagementController : ApiController - { - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - - public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) - : base(commandBus) - { - this.userManager = userManager; - this.userFactory = userFactory; - } - - [HttpGet] - [Route("user-management/")] - [ProducesResponseType(typeof(UsersDto), 200)] - [ApiPermission(Permissions.AdminUsersRead)] - public async Task GetUsers([FromQuery] string query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) - { - var taskForItems = userManager.QueryByEmailAsync(query, take, skip); - var taskForCount = userManager.CountByEmailAsync(query); - - await Task.WhenAll(taskForItems, taskForCount); - - var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this); - - return Ok(response); - } - - [HttpGet] - [Route("user-management/{id}/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersRead)] - public async Task GetUser(string id) - { - var user = await userManager.FindByIdWithClaimsAsync(id); - - if (user == null) - { - return NotFound(); - } - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPost] - [Route("user-management/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersCreate)] - public async Task PostUser([FromBody] CreateUserDto request) - { - var user = await userManager.CreateAsync(userFactory, request.ToValues()); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPut] - [Route("user-management/{id}/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersUpdate)] - public async Task PutUser(string id, [FromBody] UpdateUserDto request) - { - var user = await userManager.UpdateAsync(id, request.ToValues()); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPut] - [Route("user-management/{id}/lock/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersLock)] - public async Task LockUser(string id) - { - if (this.IsUser(id)) - { - throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself.")); - } - - var user = await userManager.LockAsync(id); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - - [HttpPut] - [Route("user-management/{id}/unlock/")] - [ProducesResponseType(typeof(UserDto), 201)] - [ApiPermission(Permissions.AdminUsersUnlock)] - public async Task UnlockUser(string id) - { - if (this.IsUser(id)) - { - throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself.")); - } - - var user = await userManager.UnlockAsync(id); - - var response = UserDto.FromUser(user, this); - - return Ok(response); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs deleted file mode 100644 index 597dc64de..000000000 --- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Squidex.Areas.Api.Controllers.Users.Models; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Users -{ - /// - /// Readonly API to retrieve information about squidex users. - /// - [ApiExplorerSettings(GroupName = nameof(Users))] - public sealed class UsersController : ApiController - { - private static readonly byte[] AvatarBytes; - private readonly IUserPictureStore userPictureStore; - private readonly IUserResolver userResolver; - private readonly ISemanticLog log; - - static UsersController() - { - var assembly = typeof(UsersController).Assembly; - - using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) - { - AvatarBytes = new byte[avatarStream.Length]; - - avatarStream.Read(AvatarBytes, 0, AvatarBytes.Length); - } - } - - public UsersController( - ICommandBus commandBus, - IUserPictureStore userPictureStore, - IUserResolver userResolver, - ISemanticLog log) - : base(commandBus) - { - this.userPictureStore = userPictureStore; - this.userResolver = userResolver; - - this.log = log; - } - - /// - /// Get the user resources. - /// - /// - /// 200 => User resources returned. - /// - [HttpGet] - [Route("/")] - [ProducesResponseType(typeof(ResourcesDto), 200)] - [ApiPermission] - public IActionResult GetUserResources() - { - var response = ResourcesDto.FromController(this); - - return Ok(response); - } - - /// - /// Get users by query. - /// - /// The query to search the user by email address. Case invariant. - /// - /// Search the user by query that contains the email address or the part of the email address. - /// - /// - /// 200 => Users returned. - /// - [HttpGet] - [Route("users/")] - [ProducesResponseType(typeof(UserDto[]), 200)] - [ApiPermission] - public async Task GetUsers(string query) - { - try - { - var users = await userResolver.QueryByEmailAsync(query); - - var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray(); - - return Ok(response); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", nameof(GetUsers)) - .WriteProperty("status", "Failed")); - } - - return Ok(new UserDto[0]); - } - - /// - /// Get user by id. - /// - /// The id of the user (GUID). - /// - /// 200 => User found. - /// 404 => User not found. - /// - [HttpGet] - [Route("users/{id}/")] - [ProducesResponseType(typeof(UserDto), 200)] - [ApiPermission] - public async Task GetUser(string id) - { - try - { - var entity = await userResolver.FindByIdOrEmailAsync(id); - - if (entity != null) - { - var response = UserDto.FromUser(entity, this); - - return Ok(response); - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", nameof(GetUser)) - .WriteProperty("status", "Failed")); - } - - return NotFound(); - } - - /// - /// Get user picture by id. - /// - /// The id of the user (GUID). - /// - /// 200 => User found and image or fallback returned. - /// 404 => User not found. - /// - [HttpGet] - [Route("users/{id}/picture/")] - [ProducesResponseType(typeof(FileResult), 200)] - [ResponseCache(Duration = 300)] - public async Task GetUserPicture(string id) - { - try - { - var entity = await userResolver.FindByIdOrEmailAsync(id); - - if (entity != null) - { - if (entity.IsPictureUrlStored()) - { - return new FileStreamResult(await userPictureStore.DownloadAsync(entity.Id), "image/png"); - } - - using (var client = new HttpClient()) - { - var url = entity.PictureNormalizedUrl(); - - if (!string.IsNullOrWhiteSpace(url)) - { - var response = await client.GetAsync(url); - - if (response.IsSuccessStatusCode) - { - var contentType = response.Content.Headers.ContentType.ToString(); - - return new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType); - } - } - } - } - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", nameof(GetUser)) - .WriteProperty("status", "Failed")); - } - - return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); - } - } -} diff --git a/src/Squidex/Areas/Api/Startup.cs b/src/Squidex/Areas/Api/Startup.cs deleted file mode 100644 index d5fb554b2..000000000 --- a/src/Squidex/Areas/Api/Startup.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Squidex.Areas.Api.Config.OpenApi; -using Squidex.Web; - -namespace Squidex.Areas.Api -{ - public static class Startup - { - public static void ConfigureApi(this IApplicationBuilder app) - { - app.Map(Constants.ApiPrefix, appApi => - { - appApi.UseMyOpenApi(); - appApi.UseMvc(); - }); - } - } -} diff --git a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs deleted file mode 100644 index 8ed930af6..000000000 --- a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Squidex.Areas.Frontend.Middlewares -{ - public sealed class WebpackMiddleware - { - private const string WebpackUrl = "http://localhost:3000/index.html"; - private readonly RequestDelegate next; - - public WebpackMiddleware(RequestDelegate next) - { - this.next = next; - } - - public async Task Invoke(HttpContext context) - { - if (context.IsIndex() && context.Response.StatusCode != 304) - { - using (var client = new HttpClient()) - { - var result = await client.GetAsync(WebpackUrl); - - context.Response.StatusCode = (int)result.StatusCode; - - if (result.IsSuccessStatusCode) - { - var html = await result.Content.ReadAsStringAsync(); - - html = html.AdjustHtml(context); - - await context.Response.WriteHtmlAsync(html); - } - } - } - else if (context.IsHtmlPath() && context.Response.StatusCode != 304) - { - var responseBuffer = new MemoryStream(); - var responseBody = context.Response.Body; - - context.Response.Body = responseBuffer; - - await next(context); - - if (context.Response.StatusCode != 304) - { - context.Response.Body = responseBody; - - var html = Encoding.UTF8.GetString(responseBuffer.ToArray()); - - html = html.AdjustHtml(context); - - context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); - context.Response.Body = responseBody; - - await context.Response.WriteAsync(html); - } - } - else - { - await next(context); - } - } - } -} diff --git a/src/Squidex/Areas/Frontend/Startup.cs b/src/Squidex/Areas/Frontend/Startup.cs deleted file mode 100644 index 875945463..000000000 --- a/src/Squidex/Areas/Frontend/Startup.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Frontend.Middlewares; -using Squidex.Pipeline.Squid; - -namespace Squidex.Areas.Frontend -{ - public static class Startup - { - public static void ConfigureFrontend(this IApplicationBuilder app) - { - var environment = app.ApplicationServices.GetRequiredService(); - - app.UseMiddleware(); - - app.Use((context, next) => - { - if (context.Request.Path == "/client-callback-popup") - { - context.Request.Path = new PathString("/client-callback-popup.html"); - } - else if (context.Request.Path == "/client-callback-silent") - { - context.Request.Path = new PathString("/client-callback-silent.html"); - } - else if (!Path.HasExtension(context.Request.Path.Value)) - { - if (environment.IsDevelopment()) - { - context.Request.Path = new PathString("/index.html"); - } - else - { - context.Request.Path = new PathString("/build/index.html"); - } - } - - return next(); - }); - - if (environment.IsDevelopment()) - { - app.UseMiddleware(); - } - else - { - app.UseMiddleware(); - } - - app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = context => - { - var response = context.Context.Response; - var responseHeaders = response.GetTypedHeaders(); - - if (!string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) - { - responseHeaders.CacheControl = new CacheControlHeaderValue - { - MaxAge = TimeSpan.FromDays(60) - }; - } - else - { - responseHeaders.CacheControl = new CacheControlHeaderValue - { - NoCache = true - }; - } - } - }); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx b/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx deleted file mode 100644 index 7a70ee610..000000000 Binary files a/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx and /dev/null differ diff --git a/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk b/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk deleted file mode 100644 index 252e41c3d..000000000 Binary files a/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk and /dev/null differ diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs deleted file mode 100644 index e88eb14a0..000000000 --- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Logging; -using Squidex.Config; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Areas.IdentityServer.Config -{ - public static class IdentityServerExtensions - { - public static IApplicationBuilder UseMyIdentityServer(this IApplicationBuilder app) - { - app.UseIdentityServer(); - - return app; - } - - public static IServiceProvider UseMyAdmin(this IServiceProvider services) - { - var options = services.GetRequiredService>().Value; - - IdentityModelEventSource.ShowPII = options.ShowPII; - - var userManager = services.GetRequiredService>(); - var userFactory = services.GetRequiredService(); - - var log = services.GetRequiredService(); - - if (options.IsAdminConfigured()) - { - var adminEmail = options.AdminEmail; - var adminPass = options.AdminPassword; - - Task.Run(async () => - { - if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) - { - try - { - var values = new UserValues - { - Email = adminEmail, - Password = adminPass, - Permissions = new PermissionSet(Permissions.Admin), - DisplayName = adminEmail - }; - - await userManager.CreateAsync(userFactory, values); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "createAdmin") - .WriteProperty("status", "failed")); - } - } - }).Wait(); - } - - return services; - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs deleted file mode 100644 index 8ede8a1bb..000000000 --- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using IdentityModel; -using IdentityServer4.Models; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.DataProtection.KeyManagement; -using Microsoft.AspNetCore.DataProtection.Repositories; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Domain.Users; -using Squidex.Shared.Identity; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer.Config -{ - public static class IdentityServerServices - { - public static void AddMyIdentityServer(this IServiceCollection services) - { - X509Certificate2 certificate; - - var assembly = typeof(IdentityServerServices).Assembly; - - using (var certStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx")) - { - var certData = new byte[certStream.Length]; - - certStream.Read(certData, 0, certData.Length); - certificate = new X509Certificate2(certData, "password", - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable); - } - - services.AddSingleton>(s => - { - return new ConfigureOptions(options => - { - options.XmlRepository = s.GetRequiredService(); - }); - }); - - services.AddDataProtection().SetApplicationName("Squidex"); - services.AddSingleton(GetApiResources()); - services.AddSingleton(GetIdentityResources()); - - services.AddIdentity() - .AddDefaultTokenProviders(); - services.AddSingleton, - PwnedPasswordValidator>(); - services.AddSingleton, - UserClaimsPrincipalFactoryWithEmail>(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddIdentityServer(options => - { - options.UserInteraction.ErrorUrl = "/error/"; - }) - .AddAspNetIdentity() - .AddInMemoryApiResources(GetApiResources()) - .AddInMemoryIdentityResources(GetIdentityResources()) - .AddSigningCredential(certificate); - } - - private static IEnumerable GetApiResources() - { - yield return new ApiResource(Constants.ApiScope) - { - UserClaims = new List - { - JwtClaimTypes.Email, - JwtClaimTypes.Role, - SquidexClaimTypes.Permissions - } - }; - } - - private static IEnumerable GetIdentityResources() - { - yield return new IdentityResources.OpenId(); - yield return new IdentityResources.Profile(); - yield return new IdentityResources.Email(); - yield return new IdentityResource(Constants.RoleScope, - new[] - { - JwtClaimTypes.Role - }); - yield return new IdentityResource(Constants.PermissionsScope, - new[] - { - SquidexClaimTypes.Permissions - }); - yield return new IdentityResource(Constants.ProfileScope, - new[] - { - SquidexClaimTypes.DisplayName, - SquidexClaimTypes.PictureUrl - }); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs deleted file mode 100644 index 33d60dc0d..000000000 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ /dev/null @@ -1,232 +0,0 @@ -// ========================================================================== -// 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.Security.Claims; -using System.Threading.Tasks; -using IdentityServer4; -using IdentityServer4.Models; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using Squidex.Config; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer.Config -{ - public class LazyClientStore : IClientStore - { - private readonly UserManager userManager; - private readonly IAppProvider appProvider; - private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public LazyClientStore( - UserManager userManager, - IOptions urlsOptions, - IOptions identityOptions, - IAppProvider appProvider) - { - Guard.NotNull(identityOptions, nameof(identityOptions)); - Guard.NotNull(urlsOptions, nameof(urlsOptions)); - Guard.NotNull(userManager, nameof(userManager)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.userManager = userManager; - this.appProvider = appProvider; - - CreateStaticClients(urlsOptions, identityOptions); - } - - public async Task FindClientByIdAsync(string clientId) - { - var client = staticClients.GetOrDefault(clientId); - - if (client != null) - { - return client; - } - - var (appName, appClientId) = clientId.GetClientParts(); - - if (!string.IsNullOrWhiteSpace(appName)) - { - var app = await appProvider.GetAppAsync(appName); - - var appClient = app?.Clients.GetOrDefault(appClientId); - - if (appClient != null) - { - return CreateClientFromApp(clientId, appClient); - } - } - - var user = await userManager.FindByIdWithClaimsAsync(clientId); - - if (!string.IsNullOrWhiteSpace(user?.ClientSecret())) - { - return CreateClientFromUser(user); - } - - return null; - } - - private static Client CreateClientFromUser(UserWithClaims user) - { - return new Client - { - ClientId = user.Id, - ClientName = $"{user.Email} Client", - ClientClaimsPrefix = null, - ClientSecrets = new List - { - new Secret(user.ClientSecret().Sha256()) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new List - { - Constants.ApiScope, - Constants.RoleScope, - Constants.PermissionsScope - }, - Claims = new List - { - new Claim(OpenIdClaims.Subject, user.Id) - } - }; - } - - private static Client CreateClientFromApp(string id, AppClient appClient) - { - return new Client - { - ClientId = id, - ClientName = id, - ClientSecrets = new List - { - new Secret(appClient.Secret.Sha256()) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new List - { - Constants.ApiScope, - Constants.RoleScope, - Constants.PermissionsScope - } - }; - } - - private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions) - { - foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value)) - { - staticClients[client.ClientId] = client; - } - } - - private static IEnumerable CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions) - { - var frontendId = Constants.FrontendClient; - - yield return new Client - { - ClientId = frontendId, - ClientName = frontendId, - RedirectUris = new List - { - urlsOptions.BuildUrl("login;"), - urlsOptions.BuildUrl("client-callback-silent", false), - urlsOptions.BuildUrl("client-callback-popup", false) - }, - PostLogoutRedirectUris = new List - { - urlsOptions.BuildUrl("logout", false) - }, - AllowAccessTokensViaBrowser = true, - AllowedGrantTypes = GrantTypes.Implicit, - AllowedScopes = new List - { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - IdentityServerConstants.StandardScopes.Email, - Constants.ApiScope, - Constants.PermissionsScope, - Constants.ProfileScope, - Constants.RoleScope - }, - RequireConsent = false - }; - - var internalClient = Constants.InternalClientId; - - yield return new Client - { - ClientId = internalClient, - ClientName = internalClient, - ClientSecrets = new List - { - new Secret(Constants.InternalClientSecret) - }, - RedirectUris = new List - { - urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false), - urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, - AllowedScopes = new List - { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - IdentityServerConstants.StandardScopes.Email, - Constants.ApiScope, - Constants.PermissionsScope, - Constants.ProfileScope, - Constants.RoleScope - }, - RequireConsent = false - }; - - if (identityOptions.IsAdminClientConfigured()) - { - var id = identityOptions.AdminClientId; - - yield return new Client - { - ClientId = id, - ClientName = id, - ClientSecrets = new List - { - new Secret(identityOptions.AdminClientSecret.Sha256()) - }, - AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, - AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new List - { - Constants.ApiScope, - Constants.RoleScope, - Constants.PermissionsScope - }, - Claims = new List - { - new Claim(SquidexClaimTypes.Permissions, Permissions.All) - } - }; - } - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs deleted file mode 100644 index fb645e1a3..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ /dev/null @@ -1,433 +0,0 @@ -// ========================================================================== -// 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 System.Runtime.CompilerServices; -using System.Security; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using IdentityServer4.Models; -using IdentityServer4.Services; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Squidex.Config; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer.Controllers.Account -{ - public sealed class AccountController : IdentityServerController - { - private readonly SignInManager signInManager; - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - private readonly IUserEvents userEvents; - private readonly MyIdentityOptions identityOptions; - private readonly ISemanticLog log; - private readonly IIdentityServerInteractionService interactions; - - public AccountController( - SignInManager signInManager, - UserManager userManager, - IUserFactory userFactory, - IUserEvents userEvents, - IOptions identityOptions, - ISemanticLog log, - IIdentityServerInteractionService interactions) - { - this.log = log; - this.userEvents = userEvents; - this.userManager = userManager; - this.userFactory = userFactory; - this.interactions = interactions; - this.identityOptions = identityOptions.Value; - this.signInManager = signInManager; - } - - [HttpGet] - [Route("account/error/")] - public IActionResult LoginError() - { - throw new InvalidOperationException(); - } - - [HttpGet] - [Route("account/forbidden/")] - public IActionResult Forbidden() - { - throw new SecurityException("User is not allowed to login."); - } - - [HttpGet] - [Route("account/lockedout/")] - public IActionResult LockedOut() - { - return View(); - } - - [HttpGet] - [Route("account/accessdenied/")] - public IActionResult AccessDenied() - { - return View(); - } - - [HttpGet] - [Route("account/logout-completed/")] - public IActionResult LogoutCompleted() - { - return View(); - } - - [HttpGet] - [Route("account/consent/")] - public IActionResult Consent(string returnUrl = null) - { - return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); - } - - [HttpPost] - [Route("account/consent/")] - public async Task Consent(ConsentModel model, string returnUrl = null) - { - if (!model.ConsentToCookies) - { - ModelState.AddModelError(nameof(model.ConsentToCookies), "You have to give consent."); - } - - if (!model.ConsentToPersonalInformation) - { - ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), "You have to give consent."); - } - - if (!ModelState.IsValid) - { - var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; - - return View(vm); - } - - var user = await userManager.GetUserWithClaimsAsync(User); - - var update = new UserValues - { - Consent = true, - ConsentForEmails = model.ConsentToAutomatedEmails - }; - - await userManager.UpdateAsync(user.Id, update); - - userEvents.OnConsentGiven(user); - - return RedirectToReturnUrl(returnUrl); - } - - [HttpGet] - [Route("account/logout/")] - public async Task Logout(string logoutId) - { - var context = await interactions.GetLogoutContextAsync(logoutId); - - await signInManager.SignOutAsync(); - - return RedirectToLogoutUrl(context); - } - - [HttpGet] - [Route("account/logout-redirect/")] - public async Task LogoutRedirect() - { - await signInManager.SignOutAsync(); - - return RedirectToAction(nameof(LogoutCompleted)); - } - - [HttpGet] - [Route("account/signup/")] - public Task Signup(string returnUrl = null) - { - return LoginViewAsync(returnUrl, false, false); - } - - [HttpGet] - [Route("account/login/")] - [ClearCookies] - public Task Login(string returnUrl = null) - { - return LoginViewAsync(returnUrl, true, false); - } - - [HttpPost] - [Route("account/login/")] - public async Task Login(LoginModel model, string returnUrl = null) - { - if (!ModelState.IsValid) - { - return await LoginViewAsync(returnUrl, true, true); - } - - var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, true, true); - - if (!result.Succeeded) - { - return await LoginViewAsync(returnUrl, true, true); - } - else - { - return RedirectToReturnUrl(returnUrl); - } - } - - private async Task LoginViewAsync(string returnUrl, bool isLogin, bool isFailed) - { - var allowPasswordAuth = identityOptions.AllowPasswordAuth; - - var externalProviders = await signInManager.GetExternalProvidersAsync(); - - if (externalProviders.Count == 1 && !allowPasswordAuth) - { - var provider = externalProviders[0].AuthenticationScheme; - - var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); - - return Challenge(properties, provider); - } - - var vm = new LoginVM - { - ExternalProviders = externalProviders, - IsLogin = isLogin, - IsFailed = isFailed, - HasPasswordAuth = allowPasswordAuth, - HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(), - ReturnUrl = returnUrl - }; - - return View(nameof(Login), vm); - } - - [HttpPost] - [Route("account/external/")] - public IActionResult External(string provider, string returnUrl = null) - { - var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); - - return Challenge(properties, provider); - } - - [HttpGet] - [Route("account/external-callback/")] - public async Task ExternalCallback(string returnUrl = null) - { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(); - - if (externalLogin == null) - { - return RedirectToAction(nameof(Login)); - } - - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); - - if (!result.Succeeded && result.IsLockedOut) - { - return View(nameof(LockedOut)); - } - - var isLoggedIn = result.Succeeded; - - UserWithClaims user; - - if (isLoggedIn) - { - user = await userManager.FindByLoginWithClaimsAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); - } - else - { - var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; - - user = await userManager.FindByEmailWithClaimsAsyncAsync(email); - - if (user != null) - { - isLoggedIn = - await AddLoginAsync(user, externalLogin) && - await AddClaimsAsync(user, externalLogin, email) && - await LoginAsync(externalLogin); - } - else - { - user = new UserWithClaims(userFactory.Create(email), new List()); - - var isFirst = userManager.Users.LongCount() == 0; - - isLoggedIn = - await AddUserAsync(user) && - await AddLoginAsync(user, externalLogin) && - await AddClaimsAsync(user, externalLogin, email, isFirst) && - await LockAsync(user, isFirst) && - await LoginAsync(externalLogin); - - userEvents.OnUserRegistered(user); - - if (await userManager.IsLockedOutAsync(user.Identity)) - { - return View(nameof(LockedOut)); - } - } - } - - if (!isLoggedIn) - { - return RedirectToAction(nameof(Login)); - } - else if (user != null && !user.HasConsent() && !identityOptions.NoConsent) - { - return RedirectToAction(nameof(Consent), new { returnUrl }); - } - else - { - return RedirectToReturnUrl(returnUrl); - } - } - - private Task AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin) - { - return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin)); - } - - private Task AddUserAsync(UserWithClaims user) - { - return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity)); - } - - private async Task LoginAsync(UserLoginInfo externalLogin) - { - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); - - return result.Succeeded; - } - - private Task LockAsync(UserWithClaims user, bool isFirst) - { - if (isFirst || !identityOptions.LockAutomatically) - { - return TaskHelper.True; - } - - return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); - } - - private Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) - { - var newClaims = new List(); - - void AddClaim(Claim claim) - { - newClaims.Add(claim); - - user.Claims.Add(claim); - } - - foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims()) - { - AddClaim(squidexClaim); - } - - if (!user.HasPictureUrl()) - { - AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email))); - } - - if (!user.HasDisplayName()) - { - AddClaim(new Claim(SquidexClaimTypes.DisplayName, email)); - } - - if (isFirst) - { - AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)); - } - - return MakeIdentityOperation(() => userManager.SyncClaimsAsync(user.Identity, newClaims)); - } - - private IActionResult RedirectToLogoutUrl(LogoutRequest context) - { - if (!string.IsNullOrWhiteSpace(context.PostLogoutRedirectUri)) - { - return Redirect(context.PostLogoutRedirectUri); - } - else - { - return Redirect("~/../"); - } - } - - private IActionResult RedirectToReturnUrl(string returnUrl) - { - if (!string.IsNullOrWhiteSpace(returnUrl)) - { - return Redirect(returnUrl); - } - else - { - return Redirect("~/../"); - } - } - - private async Task MakeIdentityOperation(Func> action, [CallerMemberName] string operationName = null) - { - try - { - var result = await action(); - - if (!result.Succeeded) - { - var errorMessageBuilder = new StringBuilder(); - - foreach (var error in result.Errors) - { - errorMessageBuilder.Append(error.Code); - errorMessageBuilder.Append(": "); - errorMessageBuilder.AppendLine(error.Description); - } - - var errorMessage = errorMessageBuilder.ToString(); - - log.LogError((operationName, errorMessage), (ctx, w) => w - .WriteProperty("action", ctx.operationName) - .WriteProperty("status", "Failed") - .WriteProperty("message", ctx.errorMessage)); - } - - return result.Succeeded; - } - catch (Exception ex) - { - log.LogError(ex, operationName, (logOperationName, w) => w - .WriteProperty("action", logOperationName) - .WriteProperty("status", "Failed")); - - return false; - } - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs deleted file mode 100644 index a8764ff13..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Areas.IdentityServer.Controllers.Account -{ - public sealed class ConsentVM - { - public string ReturnUrl { get; set; } - - public string PrivacyUrl { get; set; } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs deleted file mode 100644 index 69a029bd8..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Areas.IdentityServer.Controllers.Account -{ - public class LoginVM - { - public string ReturnUrl { get; set; } - - public bool IsLogin { get; set; } - - public bool IsFailed { get; set; } - - public bool HasPasswordAuth { get; set; } - - public bool HasPasswordAndExternal { get; set; } - - public IReadOnlyList ExternalProviders { get; set; } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs deleted file mode 100644 index 527dd12dd..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using IdentityServer4.Models; -using IdentityServer4.Services; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Squidex.Infrastructure; - -namespace Squidex.Areas.IdentityServer.Controllers.Error -{ - public sealed class ErrorController : IdentityServerController - { - private readonly IIdentityServerInteractionService interaction; - private readonly SignInManager signInManager; - - public ErrorController(IIdentityServerInteractionService interaction, SignInManager signInManager) - { - this.interaction = interaction; - this.signInManager = signInManager; - } - - [Route("error/")] - public async Task Error(string errorId = null) - { - await signInManager.SignOutAsync(); - - var vm = new ErrorViewModel(); - - if (!string.IsNullOrWhiteSpace(errorId)) - { - var message = await interaction.GetErrorContextAsync(errorId); - - if (message != null) - { - vm.Error = message; - } - } - - if (vm.Error == null) - { - var error = HttpContext.Features.Get()?.Error; - - if (error is DomainException exception) - { - vm.Error = new ErrorMessage { ErrorDescription = exception.Message }; - } - else if (error?.InnerException is DomainException exception2) - { - vm.Error = new ErrorMessage { ErrorDescription = exception2.Message }; - } - } - - return View("Error", vm); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs deleted file mode 100644 index 652c61f79..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// 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 System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Identity; - -namespace Squidex.Areas.IdentityServer.Controllers -{ - public static class Extensions - { - public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string expectedXsrf = null) - { - var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); - - var email = externalLogin.Principal.FindFirst(ClaimTypes.Email)?.Value; - - if (string.IsNullOrWhiteSpace(email)) - { - throw new InvalidOperationException("External provider does not provide email claim."); - } - - externalLogin.ProviderDisplayName = email; - - return externalLogin; - } - - public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) - { - var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); - - var externalProviders = - externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) - .Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); - - return externalProviders; - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs deleted file mode 100644 index e28258681..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ /dev/null @@ -1,224 +0,0 @@ -// ========================================================================== -// 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.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Squidex.Config; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; - -namespace Squidex.Areas.IdentityServer.Controllers.Profile -{ - [Authorize] - public sealed class ProfileController : IdentityServerController - { - private readonly SignInManager signInManager; - private readonly UserManager userManager; - private readonly IUserPictureStore userPictureStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly MyIdentityOptions identityOptions; - - public ProfileController( - SignInManager signInManager, - UserManager userManager, - IUserPictureStore userPictureStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IOptions identityOptions) - { - this.signInManager = signInManager; - this.identityOptions = identityOptions.Value; - this.userManager = userManager; - this.userPictureStore = userPictureStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - } - - [HttpGet] - [Route("/account/profile/")] - public async Task Profile(string successMessage = null) - { - var user = await userManager.GetUserWithClaimsAsync(User); - - return View(await GetProfileVM(user, successMessage: successMessage)); - } - - [HttpPost] - [Route("/account/profile/login-add/")] - public async Task AddLogin(string provider) - { - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - - var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(AddLoginCallback)), userManager.GetUserId(User)); - - return Challenge(properties, provider); - } - - [HttpGet] - [Route("/account/profile/login-add-callback/")] - public Task AddLoginCallback() - { - return MakeChangeAsync(user => AddLoginAsync(user), - "Login added successfully."); - } - - [HttpPost] - [Route("/account/profile/update/")] - public Task UpdateProfile(ChangeProfileModel model) - { - return MakeChangeAsync(user => userManager.UpdateSafeAsync(user.Identity, model.ToValues()), - "Account updated successfully."); - } - - [HttpPost] - [Route("/account/profile/login-remove/")] - public Task RemoveLogin(RemoveLoginModel model) - { - return MakeChangeAsync(user => userManager.RemoveLoginAsync(user.Identity, model.LoginProvider, model.ProviderKey), - "Login provider removed successfully."); - } - - [HttpPost] - [Route("/account/profile/password-set/")] - public Task SetPassword(SetPasswordModel model) - { - return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password), - "Password set successfully."); - } - - [HttpPost] - [Route("/account/profile/password-change/")] - public Task ChangePassword(ChangePasswordModel model) - { - return MakeChangeAsync(user => userManager.ChangePasswordAsync(user.Identity, model.OldPassword, model.Password), - "Password changed successfully."); - } - - [HttpPost] - [Route("/account/profile/generate-client-secret/")] - public Task GenerateClientSecret() - { - return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity), - "Client secret generated successfully."); - } - - [HttpPost] - [Route("/account/profile/upload-picture/")] - public Task UploadPicture(List file) - { - return MakeChangeAsync(user => UpdatePictureAsync(file, user), - "Picture uploaded successfully."); - } - - private async Task AddLoginAsync(UserWithClaims user) - { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User)); - - return await userManager.AddLoginAsync(user.Identity, externalLogin); - } - - private async Task UpdatePictureAsync(List file, UserWithClaims user) - { - if (file.Count != 1) - { - return IdentityResult.Failed(new IdentityError { Description = "Please upload a single file." }); - } - - using (var thumbnailStream = new MemoryStream()) - { - try - { - await assetThumbnailGenerator.CreateThumbnailAsync(file[0].OpenReadStream(), thumbnailStream, 128, 128, "Crop"); - - thumbnailStream.Position = 0; - } - catch - { - return IdentityResult.Failed(new IdentityError { Description = "Picture is not a valid image." }); - } - - await userPictureStore.UploadAsync(user.Id, thumbnailStream); - } - - return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }); - } - - private async Task MakeChangeAsync(Func> action, string successMessage, ChangeProfileModel model = null) - { - var user = await userManager.GetUserWithClaimsAsync(User); - - if (!ModelState.IsValid) - { - return View(nameof(Profile), await GetProfileVM(user, model)); - } - - string errorMessage; - try - { - var result = await action(user); - - if (result.Succeeded) - { - await signInManager.SignInAsync(user.Identity, true); - - return RedirectToAction(nameof(Profile), new { successMessage }); - } - - errorMessage = string.Join(". ", result.Errors.Select(x => x.Description)); - } - catch - { - errorMessage = "An unexpected exception occurred."; - } - - return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); - } - - private async Task GetProfileVM(UserWithClaims user, ChangeProfileModel model = null, string errorMessage = null, string successMessage = null) - { - var taskForProviders = signInManager.GetExternalProvidersAsync(); - var taskForPassword = userManager.HasPasswordAsync(user.Identity); - var taskForLogins = userManager.GetLoginsAsync(user.Identity); - - await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins); - - var result = new ProfileVM - { - Id = user.Id, - ClientSecret = user.ClientSecret(), - Email = user.Email, - ErrorMessage = errorMessage, - ExternalLogins = taskForLogins.Result, - ExternalProviders = taskForProviders.Result, - DisplayName = user.DisplayName(), - IsHidden = user.IsHidden(), - HasPassword = taskForPassword.Result, - HasPasswordAuth = identityOptions.AllowPasswordAuth, - SuccessMessage = successMessage - }; - - if (model != null) - { - SimpleMapper.Map(model, result); - } - - return result; - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs deleted file mode 100644 index e895f70e9..000000000 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.AspNetCore.Identity; - -namespace Squidex.Areas.IdentityServer.Controllers.Profile -{ - public sealed class ProfileVM - { - public string Id { get; set; } - - public string Email { get; set; } - - public string DisplayName { get; set; } - - public string ClientSecret { get; set; } - - public string ErrorMessage { get; set; } - - public string SuccessMessage { get; set; } - - public bool IsHidden { get; set; } - - public bool HasPassword { get; set; } - - public bool HasPasswordAuth { get; set; } - - public IList ExternalLogins { get; set; } - - public IList ExternalProviders { get; set; } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Startup.cs b/src/Squidex/Areas/IdentityServer/Startup.cs deleted file mode 100644 index d46ab509e..000000000 --- a/src/Squidex/Areas/IdentityServer/Startup.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Areas.IdentityServer.Config; -using Squidex.Web; - -namespace Squidex.Areas.IdentityServer -{ - public static class Startup - { - public static void ConfigureIdentityServer(this IApplicationBuilder app) - { - app.ApplicationServices.UseMyAdmin(); - - var environment = app.ApplicationServices.GetRequiredService(); - - app.Map(Constants.IdentityServerPrefix, identityApp => - { - if (!environment.IsDevelopment()) - { - identityApp.UseDeveloperExceptionPage(); - } - else - { - identityApp.UseExceptionHandler("/error"); - } - - identityApp.UseMyIdentityServer(); - - identityApp.UseMvc(); - }); - } - } -} diff --git a/src/Squidex/Areas/IdentityServer/Views/Extensions.cs b/src/Squidex/Areas/IdentityServer/Views/Extensions.cs deleted file mode 100644 index 814f9fb2a..000000000 --- a/src/Squidex/Areas/IdentityServer/Views/Extensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Squidex.Areas.IdentityServer.Views -{ - public static class Extensions - { - public static string RootContentUrl(this IUrlHelper urlHelper, string contentPath) - { - if (string.IsNullOrEmpty(contentPath)) - { - return null; - } - - if (contentPath[0] == '~') - { - var segment = new PathString(contentPath.Substring(1)); - - var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase; - - if (applicationPath.HasValue) - { - var indexOfLastPart = applicationPath.Value.LastIndexOf('/'); - - if (indexOfLastPart >= 0) - { - applicationPath = applicationPath.Value.Substring(0, indexOfLastPart); - } - } - - return applicationPath.Add(segment).Value; - } - - return contentPath; - } - } -} diff --git a/src/Squidex/Areas/OrleansDashboard/Startup.cs b/src/Squidex/Areas/OrleansDashboard/Startup.cs deleted file mode 100644 index 943057450..000000000 --- a/src/Squidex/Areas/OrleansDashboard/Startup.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Orleans; -using Squidex.Areas.OrleansDashboard.Middlewares; -using Squidex.Web; - -namespace Squidex.Areas.OrleansDashboard -{ - public static class Startup - { - public static void ConfigureOrleansDashboard(this IApplicationBuilder app) - { - app.Map(Constants.OrleansPrefix, orleansApp => - { - orleansApp.UseAuthentication(); - orleansApp.UseMiddleware(); - orleansApp.UseOrleansDashboard(); - }); - } - } -} diff --git a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs deleted file mode 100644 index dfaa742ce..000000000 --- a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities.Apps.Services; - -namespace Squidex.Areas.Portal.Middlewares -{ - public sealed class PortalRedirectMiddleware - { - private readonly IAppPlanBillingManager appPlansBillingManager; - - public PortalRedirectMiddleware(RequestDelegate next, IAppPlanBillingManager appPlansBillingManager) - { - this.appPlansBillingManager = appPlansBillingManager; - } - - public async Task Invoke(HttpContext context) - { - if (context.Request.Path == "/") - { - var userIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier); - - if (userIdClaim != null) - { - context.Response.RedirectToAbsoluteUrl(await appPlansBillingManager.GetPortalLinkAsync(userIdClaim.Value)); - } - } - } - } -} diff --git a/src/Squidex/Areas/Portal/Startup.cs b/src/Squidex/Areas/Portal/Startup.cs deleted file mode 100644 index 88cc7646b..000000000 --- a/src/Squidex/Areas/Portal/Startup.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Builder; -using Squidex.Areas.Portal.Middlewares; -using Squidex.Web; - -namespace Squidex.Areas.Portal -{ - public static class Startup - { - public static void ConfigurePortal(this IApplicationBuilder app) - { - app.Map(Constants.PortalPrefix, portalApp => - { - portalApp.UseAuthentication(); - portalApp.UseMiddleware(); - portalApp.UseMiddleware(); - }); - } - } -} diff --git a/src/Squidex/Config/Authentication/AuthenticationServices.cs b/src/Squidex/Config/Authentication/AuthenticationServices.cs deleted file mode 100644 index c086282f9..000000000 --- a/src/Squidex/Config/Authentication/AuthenticationServices.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class AuthenticationServices - { - public static void AddMyAuthentication(this IServiceCollection services, IConfiguration config) - { - var identityOptions = config.GetSection("identity").Get(); - - services.AddAuthentication() - .AddMyCookie() - .AddMyExternalGithubAuthentication(identityOptions) - .AddMyExternalGoogleAuthentication(identityOptions) - .AddMyExternalMicrosoftAuthentication(identityOptions) - .AddMyExternalOdic(identityOptions) - .AddMyIdentityServerAuthentication(identityOptions, config); - } - - public static AuthenticationBuilder AddMyCookie(this AuthenticationBuilder builder) - { - builder.Services.ConfigureApplicationCookie(options => - { - options.Cookie.Name = ".sq.auth"; - }); - - return builder.AddCookie(); - } - } -} diff --git a/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs b/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs deleted file mode 100644 index 2a618d892..000000000 --- a/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class GithubAuthenticationServices - { - public static AuthenticationBuilder AddMyExternalGithubAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsGithubAuthConfigured()) - { - authBuilder.AddGitHub(options => - { - options.ClientId = identityOptions.GithubClient; - options.ClientSecret = identityOptions.GithubSecret; - options.Events = new GithubHandler(); - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs b/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs deleted file mode 100644 index 50a3d77a1..000000000 --- a/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class GoogleAuthenticationServices - { - public static AuthenticationBuilder AddMyExternalGoogleAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsGoogleAuthConfigured()) - { - authBuilder.AddGoogle(options => - { - options.ClientId = identityOptions.GoogleClient; - options.ClientSecret = identityOptions.GoogleSecret; - options.Events = new GoogleHandler(); - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/GoogleHandler.cs b/src/Squidex/Config/Authentication/GoogleHandler.cs deleted file mode 100644 index 9b3832f6b..000000000 --- a/src/Squidex/Config/Authentication/GoogleHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OAuth; -using Squidex.Infrastructure.Tasks; -using Squidex.Shared.Identity; - -namespace Squidex.Config.Authentication -{ - public sealed class GoogleHandler : OAuthEvents - { - public override Task RedirectToAuthorizationEndpoint(RedirectContext context) - { - context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - - return TaskHelper.Done; - } - - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var nameClaim = context.Identity.FindFirst(ClaimTypes.Name)?.Value; - - if (!string.IsNullOrWhiteSpace(nameClaim)) - { - context.Identity.SetDisplayName(nameClaim); - } - - var pictureUrl = context.User?.Value("picture"); - - if (string.IsNullOrWhiteSpace(pictureUrl)) - { - pictureUrl = context.User?["image"]?.Value("url"); - - if (pictureUrl != null && pictureUrl.EndsWith("?sz=50", System.StringComparison.Ordinal)) - { - pictureUrl = pictureUrl.Substring(0, pictureUrl.Length - 6); - } - } - - if (!string.IsNullOrWhiteSpace(pictureUrl)) - { - context.Identity.SetPictureUrl(pictureUrl); - } - - return base.CreatingTicket(context); - } - } -} diff --git a/src/Squidex/Config/Authentication/IdentityServerServices.cs b/src/Squidex/Config/Authentication/IdentityServerServices.cs deleted file mode 100644 index f317247d3..000000000 --- a/src/Squidex/Config/Authentication/IdentityServerServices.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure; -using Squidex.Web; - -namespace Squidex.Config.Authentication -{ - public static class IdentityServerServices - { - public static AuthenticationBuilder AddMyIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) - { - var apiScope = Constants.ApiScope; - - var urlsOptions = config.GetSection("urls").Get(); - - if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl)) - { - string apiAuthorityUrl; - - if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl)) - { - apiAuthorityUrl = identityOptions.AuthorityUrl.BuildFullUrl(Constants.IdentityServerPrefix); - } - else - { - apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix); - } - - authBuilder.AddIdentityServerAuthentication(options => - { - options.Authority = apiAuthorityUrl; - options.ApiName = apiScope; - options.ApiSecret = null; - options.RequireHttpsMetadata = identityOptions.RequiresHttps; - }); - - authBuilder.AddOpenIdConnect(options => - { - options.Authority = apiAuthorityUrl; - options.ClientId = Constants.InternalClientId; - options.ClientSecret = Constants.InternalClientSecret; - options.CallbackPath = "/signin-internal"; - options.RequireHttpsMetadata = identityOptions.RequiresHttps; - options.SaveTokens = true; - options.Scope.Add(Constants.PermissionsScope); - options.Scope.Add(Constants.ProfileScope); - options.Scope.Add(Constants.RoleScope); - options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs b/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs deleted file mode 100644 index ea2091810..000000000 --- a/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class MicrosoftAuthenticationServices - { - public static AuthenticationBuilder AddMyExternalMicrosoftAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsMicrosoftAuthConfigured()) - { - authBuilder.AddMicrosoftAccount(options => - { - options.ClientId = identityOptions.MicrosoftClient; - options.ClientSecret = identityOptions.MicrosoftSecret; - options.Events = new MicrosoftHandler(); - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/src/Squidex/Config/Authentication/MicrosoftHandler.cs deleted file mode 100644 index 168995ad9..000000000 --- a/src/Squidex/Config/Authentication/MicrosoftHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OAuth; -using Squidex.Shared.Identity; - -namespace Squidex.Config.Authentication -{ - public sealed class MicrosoftHandler : OAuthEvents - { - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var displayName = context.User.Value("displayName"); - - if (!string.IsNullOrEmpty(displayName)) - { - context.Identity.SetDisplayName(displayName); - } - - var id = context.User.Value("id"); - - if (!string.IsNullOrEmpty(id)) - { - var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; - - context.Identity.SetPictureUrl(pictureUrl); - } - - return base.CreatingTicket(context); - } - } -} diff --git a/src/Squidex/Config/Authentication/OidcServices.cs b/src/Squidex/Config/Authentication/OidcServices.cs deleted file mode 100644 index 4a490ca97..000000000 --- a/src/Squidex/Config/Authentication/OidcServices.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.Extensions.DependencyInjection; - -namespace Squidex.Config.Authentication -{ - public static class OidcServices - { - public static AuthenticationBuilder AddMyExternalOdic(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) - { - if (identityOptions.IsOidcConfigured()) - { - var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; - - authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => - { - options.Authority = identityOptions.OidcAuthority; - options.ClientId = identityOptions.OidcClient; - options.ClientSecret = identityOptions.OidcSecret; - options.RequireHttpsMetadata = false; - options.Events = new OidcHandler(identityOptions); - - if (identityOptions.OidcScopes != null) - { - foreach (var scope in identityOptions.OidcScopes) - { - options.Scope.Add(scope); - } - } - }); - } - - return authBuilder; - } - } -} diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs deleted file mode 100644 index 90b5ac434..000000000 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using FluentFTP; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using MongoDB.Driver.GridFS; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Assets.ImageSharp; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Domain -{ - public static class AssetServices - { - public static void AddMyAssetServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("assetStore:type", new Alternatives - { - ["Default"] = () => - { - services.AddSingletonAs() - .AsOptional(); - }, - ["Folder"] = () => - { - var path = config.GetRequiredValue("assetStore:folder:path"); - - services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) - .As(); - }, - ["GoogleCloud"] = () => - { - var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); - - services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) - .As(); - }, - ["AzureBlob"] = () => - { - var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"); - var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); - - services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) - .As(); - }, - ["MongoDb"] = () => - { - var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); - var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); - - services.AddSingletonAs(c => - { - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); - var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - - var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions - { - BucketName = mongoGridFsBucketName - }); - - return new MongoGridFsAssetStore(gridFsbucket); - }) - .As(); - }, - ["Ftp"] = () => - { - var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); - var serverPort = config.GetOptionalValue("assetStore:ftp:serverPort", 21); - - var username = config.GetRequiredValue("assetStore:ftp:username"); - var password = config.GetRequiredValue("assetStore:ftp:password"); - - var path = config.GetOptionalValue("assetStore:ftp:path", "/"); - - services.AddSingletonAs(c => - { - var factory = new Func(() => new FtpClient(serverHost, serverPort, username, password)); - - return new FTPAssetStore(factory, path, c.GetRequiredService()); - }) - .As(); - } - }); - - services.AddSingletonAs() - .As(); - } - } -} diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs deleted file mode 100644 index cc8cb02f8..000000000 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ /dev/null @@ -1,371 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL; -using GraphQL.DataLoader; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Migrate_01; -using Migrate_01.Migrations; -using Orleans; -using Squidex.Areas.Api.Controllers.UI; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Apps.Invitation; -using Squidex.Domain.Apps.Entities.Apps.Templates; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Assets.Queries; -using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Domain.Apps.Entities.Contents.Queries; -using Squidex.Domain.Apps.Entities.Contents.Text; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Entities.History.Notifications; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Entities.Rules.Queries; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Domain.Apps.Entities.Tags; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Email; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Orleans; -using Squidex.Web; -using Squidex.Web.CommandMiddlewares; -using Squidex.Web.Services; - -namespace Squidex.Config.Domain -{ - public static class EntitiesServices - { - public static void AddMyEntitiesServices(this IServiceCollection services, IConfiguration config) - { - var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); - - services.AddSingletonAs(c => new UrlGenerator( - c.GetRequiredService>(), - c.GetRequiredService(), - exposeSourceUrl)) - .As().As().As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t))) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs(c => new Lazy(() => c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As>(); - - services.AddSingletonAs() - .As>(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddCommandPipeline(); - services.AddBackupHandlers(); - - services.AddSingleton>(DomainObjectGrainFormatter.Format); - - services.AddSingleton(c => - { - var uiOptions = c.GetRequiredService>(); - - var result = new InitialPatterns(); - - foreach (var (key, value) in uiOptions.Value.RegexSuggestions) - { - if (!string.IsNullOrWhiteSpace(key) && - !string.IsNullOrWhiteSpace(value)) - { - result[Guid.NewGuid()] = new AppPattern(key, value); - } - } - - return result; - }); - - var emailOptions = config.GetSection("email:smtp").Get(); - - if (emailOptions.IsConfigured()) - { - services.AddSingleton(Options.Create(emailOptions)); - - services.Configure( - config.GetSection("email:notifications")); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - } - else - { - services.AddSingletonAs() - .AsOptional(); - } - - services.AddSingletonAs() - .As(); - } - - private static void AddCommandPipeline(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingleton(typeof(IEventEnricher<>), typeof(SquidexEventEnricher<>)); - } - - private static void AddBackupHandlers(this IServiceCollection services) - { - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - } - - public static void AddMyMigrationServices(this IServiceCollection services) - { - services.AddSingletonAs() - .AsSelf(); - - services.AddTransientAs() - .AsSelf(); - - services.AddTransientAs() - .AsSelf(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - } - } -} diff --git a/src/Squidex/Config/Domain/EventPublishersServices.cs b/src/Squidex/Config/Domain/EventPublishersServices.cs deleted file mode 100644 index 623ab9ca5..000000000 --- a/src/Squidex/Config/Domain/EventPublishersServices.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; - -namespace Squidex.Config.Domain -{ - public static class EventPublishersServices - { - public static void AddMyEventPublishersServices(this IServiceCollection services, IConfiguration config) - { - var eventPublishers = config.GetSection("eventPublishers"); - - foreach (var child in eventPublishers.GetChildren()) - { - var eventPublisherType = child.GetValue("type"); - - if (string.IsNullOrWhiteSpace(eventPublisherType)) - { - throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'."); - } - - var eventsFilter = child.GetValue("eventsFilter"); - - var enabled = child.GetValue("enabled"); - - if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) - { - var publisherConfig = child.GetValue("configuration"); - - if (string.IsNullOrWhiteSpace(publisherConfig)) - { - throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'."); - } - - var exchange = child.GetValue("exchange"); - - if (string.IsNullOrWhiteSpace(exchange)) - { - throw new ConfigurationException($"Configure EventPublisher RabbitMq exchange with 'eventPublishers:{child.Key}:configuration'."); - } - - var name = $"EventPublishers_{child.Key}"; - - if (enabled) - { - services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) - .As(); - } - } - else - { - throw new ConfigurationException($"Unsupported value '{child.Key}' for 'eventPublishers:{child.Key}:type', supported: RabbitMq."); - } - } - } - } -} diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs deleted file mode 100644 index 3f9158605..000000000 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using EventStore.ClientAPI; -using Microsoft.Azure.Documents.Client; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Newtonsoft.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.States; - -namespace Squidex.Config.Domain -{ - public static class EventStoreServices - { - public static void AddMyEventStoreServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("eventStore:type", new Alternatives - { - ["MongoDb"] = () => - { - var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database"); - - services.AddSingletonAs(c => - { - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); - var mongDatabase = mongoClient.GetDatabase(mongoDatabaseName); - - return new MongoEventStore(mongDatabase, c.GetRequiredService()); - }) - .As(); - }, - ["CosmosDb"] = () => - { - var cosmosDbConfiguration = config.GetRequiredValue("eventStore:cosmosDB:configuration"); - var cosmosDbMasterKey = config.GetRequiredValue("eventStore:cosmosDB:masterKey"); - var cosmosDbDatabase = config.GetRequiredValue("eventStore:cosmosDB:database"); - - services.AddSingletonAs(c => new DocumentClient(new Uri(cosmosDbConfiguration), cosmosDbMasterKey, c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs(c => new CosmosDbEventStore( - c.GetRequiredService(), - cosmosDbMasterKey, - cosmosDbDatabase, - c.GetRequiredService())) - .As(); - - services.AddHealthChecks() - .AddCheck("CosmosDB", tags: new[] { "node" }); - }, - ["GetEventStore"] = () => - { - var eventStoreConfiguration = config.GetRequiredValue("eventStore:getEventStore:configuration"); - var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost"); - var eventStorePrefix = config.GetValue("eventStore:getEventStore:prefix"); - - services.AddSingletonAs(_ => EventStoreConnection.Create(eventStoreConfiguration)) - .As(); - - services.AddSingletonAs(c => new GetEventStore( - c.GetRequiredService(), - c.GetRequiredService(), - eventStorePrefix, - eventStoreProjectionHost)) - .As(); - - services.AddHealthChecks() - .AddCheck("EventStore", tags: new[] { "node" }); - } - }); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs(c => - { - var allEventConsumers = c.GetServices(); - - return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); - }); - } - } -} diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs deleted file mode 100644 index e47181641..000000000 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.DataProtection.Repositories; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Squidex.Areas.Api.Controllers.News.Service; -using Squidex.Domain.Apps.Entities.Apps.Diagnostics; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Translations; -using Squidex.Infrastructure.UsageTracking; -using Squidex.Shared.Users; - -#pragma warning disable RECS0092 // Convert field to readonly - -namespace Squidex.Config.Domain -{ - public static class InfrastructureServices - { - public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config) - { - services.AddHealthChecks() - .AddCheck("GC", tags: new[] { "node" }) - .AddCheck("Orleans", tags: new[] { "cluster" }) - .AddCheck("Orleans App", tags: new[] { "cluster" }); - - services.AddSingletonAs(c => new CachingUsageTracker( - c.GetRequiredService(), - c.GetRequiredService())) - .As(); - - services.AddSingletonAs(_ => SystemClock.Instance) - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .As(); - } - } -} diff --git a/src/Squidex/Config/Domain/LoggingExtensions.cs b/src/Squidex/Config/Domain/LoggingExtensions.cs deleted file mode 100644 index c11379044..000000000 --- a/src/Squidex/Config/Domain/LoggingExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// 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 Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Domain -{ - public static class LoggingExtensions - { - public static void LogConfiguration(this IServiceProvider services) - { - var log = services.GetRequiredService(); - - log.LogInformation(w => w - .WriteProperty("message", "Application started") - .WriteObject("environment", c => - { - var config = services.GetRequiredService(); - - var logged = new HashSet(StringComparer.OrdinalIgnoreCase); - - var orderedConfigs = config.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); - - foreach (var (key, val) in orderedConfigs) - { - if (logged.Add(key)) - { - c.WriteProperty(key.ToLowerInvariant(), val); - } - } - })); - } - } -} diff --git a/src/Squidex/Config/Domain/LoggingServices.cs b/src/Squidex/Config/Domain/LoggingServices.cs deleted file mode 100644 index f78858a8c..000000000 --- a/src/Squidex/Config/Domain/LoggingServices.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure.Log; -using Squidex.Web.Pipeline; - -namespace Squidex.Config.Domain -{ - public static class LoggingServices - { - public static void AddMyLoggingServices(this IServiceCollection services, IConfiguration config) - { - if (config.GetValue("logging:human")) - { - services.AddSingletonAs(_ => JsonLogWriterFactory.Readable()) - .As(); - } - else - { - services.AddSingletonAs(_ => JsonLogWriterFactory.Default()) - .As(); - } - - var loggingFile = config.GetValue("logging:file"); - - if (!string.IsNullOrWhiteSpace(loggingFile)) - { - services.AddSingletonAs(_ => new FileChannel(loggingFile)) - .As(); - } - - var useColors = config.GetValue("logging:colors"); - - services.AddSingletonAs(_ => new ConsoleLogChannel(useColors)) - .As(); - - services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - } - } -} diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs deleted file mode 100644 index 0e023d931..000000000 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Config.Domain -{ - public static class RuleServices - { - public static void AddMyRuleServices(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .As().AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - } - } -} diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs deleted file mode 100644 index 20c03a60c..000000000 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Migrate_01; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps.Json; -using Squidex.Domain.Apps.Core.Contents.Json; -using Squidex.Domain.Apps.Core.Rules.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Json; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Config.Domain -{ - public static class SerializationServices - { - private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) - { - settings.Converters.Add(new StringEnumConverter()); - - settings.ContractResolver = new ConverterContractResolver( - new AppClientsConverter(), - new AppContributorsConverter(), - new AppPatternsConverter(), - new ClaimsPrincipalConverter(), - new ContentFieldDataConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new InstantConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new LanguagesConfigConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new RolesConverter(), - new RuleConverter(), - new SchemaConverter(), - new StatusConverter(), - new StringEnumConverter(), - new WorkflowConverter(), - new WorkflowTransitionConverter()); - - settings.NullValueHandling = NullValueHandling.Ignore; - - settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; - settings.DateParseHandling = DateParseHandling.None; - - settings.TypeNameHandling = typeNameHandling; - - return settings; - } - - public static IServiceCollection AddMySerializers(this IServiceCollection services) - { - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs(c => JsonSerializer.Create(c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs(c => - { - var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); - - var typeNameRegistry = c.GetService(); - - if (typeNameRegistry != null) - { - serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - } - - return serializerSettings; - }).As(); - - return services; - } - - public static IMvcBuilder AddMySerializers(this IMvcBuilder mvc) - { - mvc.AddJsonOptions(options => - { - ConfigureJson(options.SerializerSettings, TypeNameHandling.None); - }); - - return mvc; - } - } -} diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs deleted file mode 100644 index 7f1149f24..000000000 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Migrate_01.Migrations.MongoDb; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.Contents.Text; -using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Entities.MongoDb.Assets; -using Squidex.Domain.Apps.Entities.MongoDb.Contents; -using Squidex.Domain.Apps.Entities.MongoDb.History; -using Squidex.Domain.Apps.Entities.MongoDb.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Users; -using Squidex.Domain.Users.MongoDb; -using Squidex.Domain.Users.MongoDb.Infrastructure; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.UsageTracking; - -namespace Squidex.Config.Domain -{ - public static class StoreServices - { - public static void AddMyStoreServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("store:type", new Alternatives - { - ["MongoDB"] = () => - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); - - services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); - - services.AddSingletonAs(_ => Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s))) - .As(); - - services.AddSingletonAs(c => c.GetRequiredService().GetDatabase(mongoDatabaseName)) - .As(); - - services.AddTransientAs(c => new DeleteContentCollections(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) - .As(); - - services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddHealthChecks() - .AddCheck("MongoDB", tags: new[] { "node" }); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As>(); - - services.AddSingletonAs() - .As>() - .As(); - - services.AddSingletonAs() - .As() - .As>(); - - services.AddSingletonAs(c => new MongoContentRepository( - c.GetRequiredService().GetDatabase(mongoContentDatabaseName), - c.GetRequiredService(), - c.GetRequiredService(), - c.GetRequiredService(), - c.GetRequiredService())) - .As() - .As>() - .As(); - - var registration = services.FirstOrDefault(x => x.ServiceType == typeof(IPersistedGrantStore)); - - if (registration == null || registration.ImplementationType == typeof(InMemoryPersistedGrantStore)) - { - services.AddSingletonAs() - .As(); - } - } - }); - - services.AddSingleton(typeof(IStore<>), typeof(Store<>)); - } - } -} diff --git a/src/Squidex/Config/Domain/SubscriptionServices.cs b/src/Squidex/Config/Domain/SubscriptionServices.cs deleted file mode 100644 index 82e370e7b..000000000 --- a/src/Squidex/Config/Domain/SubscriptionServices.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Web; - -namespace Squidex.Config.Domain -{ - public static class SubscriptionServices - { - public static void AddMySubscriptionServices(this IServiceCollection services, IConfiguration config) - { - services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - - services.AddSingletonAs() - .AsOptional(); - } - } -} diff --git a/src/Squidex/Config/Logging.cs b/src/Squidex/Config/Logging.cs deleted file mode 100644 index df98f9edc..000000000 --- a/src/Squidex/Config/Logging.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#define LOG_ALL_IDENTITY_SERVER_NONE - -using System; -using Microsoft.Extensions.Logging; - -namespace Squidex.Config -{ - public static class Logging - { - public static void AddFilters(this ILoggingBuilder builder) - { - builder.AddFilter((category, level) => - { - if (level < LogLevel.Information) - { - return false; - } - - if (category.StartsWith("Orleans.", StringComparison.OrdinalIgnoreCase)) - { - var subCategory = category.AsSpan().Slice(8); - - if (subCategory.StartsWith("Runtime.")) - { - var subCategory2 = subCategory.Slice(8); - - if (subCategory.StartsWith("NoOpHostEnvironmentStatistics", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Error; - } - - if (subCategory.StartsWith("SafeTimer", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Error; - } - } - - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } -#if LOG_ALL_IDENTITY_SERVER - if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase)) - { - return true; - } -#endif - return true; - }); - } - } -} diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs deleted file mode 100644 index a00d44982..000000000 --- a/src/Squidex/Config/Orleans/OrleansServices.cs +++ /dev/null @@ -1,132 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Configuration; -using Orleans.Hosting; -using Orleans.Providers.MongoDB.Configuration; -using OrleansDashboard; -using Squidex.Domain.Apps.Entities; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Web; -using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; - -namespace Squidex.Config.Orleans -{ - public static class OrleansServices - { - public static IServiceCollection AddOrleans(this IServiceCollection services, IConfiguration config, IWebHostEnvironment environment) - { - services.AddOrleans(config, environment, builder => - { - builder.ConfigureServices(siloServices => - { - siloServices.AddSingleton(); - siloServices.AddScoped(); - - siloServices.AddScoped(typeof(IGrainState<>), typeof(GrainState<>)); - }); - - builder.ConfigureApplicationParts(parts => - { - parts.AddApplicationPart(SquidexEntities.Assembly); - parts.AddApplicationPart(SquidexInfrastructure.Assembly); - }); - - builder.Configure(options => - { - options.Configure(); - }); - - builder.Configure(options => - { - options.FastKillOnProcessExit = false; - }); - - builder.Configure(options => - { - options.HideTrace = true; - }); - - builder.UseDashboard(options => - { - options.HostSelf = false; - }); - - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - - var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); - var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000); - - var address = Helper.ResolveIPAddressAsync(Dns.GetHostName(), AddressFamily.InterNetwork).Result; - - builder.ConfigureEndpoints( - address, - orleansPortSilo, - orleansPortGateway, - true); - - config.ConfigureByOption("orleans:clustering", new Alternatives - { - ["MongoDB"] = () => - { - builder.UseMongoDBClustering(options => - { - options.Configure(config); - }); - }, - ["Development"] = () => - { - builder.UseDevelopmentClustering(new IPEndPoint(address, orleansPortSilo)); - - builder.Configure(options => - { - options.ExpectedClusterSize = 1; - }); - } - }); - - config.ConfigureByOption("store:type", new Alternatives - { - ["MongoDB"] = () => - { - builder.UseMongoDBReminders(options => - { - options.Configure(config); - }); - } - }); - }); - - return services; - } - - private static void Configure(this MongoDBOptions options, IConfiguration config) - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - } - - private static void Configure(this ClusterOptions options) - { - options.ClusterId = Constants.OrleansClusterId; - options.ServiceId = Constants.OrleansClusterId; - } - } -} diff --git a/src/Squidex/Config/Startup/BackgroundHost.cs b/src/Squidex/Config/Startup/BackgroundHost.cs deleted file mode 100644 index 41bef8f72..000000000 --- a/src/Squidex/Config/Startup/BackgroundHost.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. - -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Startup -{ - public sealed class BackgroundHost : SafeHostedService - { - private readonly IEnumerable targets; - - public BackgroundHost(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) - : base(lifetime, log) - { - this.targets = targets; - } - - protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) - { - foreach (var target in targets.Distinct()) - { - await target.StartAsync(ct); - - log.LogInformation(w => w.WriteProperty("backgroundSystem", target.ToString())); - } - } - } -} diff --git a/src/Squidex/Config/Startup/InitializerHost.cs b/src/Squidex/Config/Startup/InitializerHost.cs deleted file mode 100644 index 9d8e2790a..000000000 --- a/src/Squidex/Config/Startup/InitializerHost.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Startup -{ - public sealed class InitializerHost : SafeHostedService - { - private readonly IEnumerable targets; - - public InitializerHost(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) - : base(lifetime, log) - { - this.targets = targets; - } - - protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) - { - foreach (var target in targets.Distinct()) - { - await target.InitializeAsync(ct); - - log.LogInformation(w => w.WriteProperty("initializedSystem", target.GetType().Name)); - } - } - } -} diff --git a/src/Squidex/Config/Startup/MigrationRebuilderHost.cs b/src/Squidex/Config/Startup/MigrationRebuilderHost.cs deleted file mode 100644 index 8a1c84ea0..000000000 --- a/src/Squidex/Config/Startup/MigrationRebuilderHost.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Migrate_01; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Startup -{ - public sealed class MigrationRebuilderHost : SafeHostedService - { - private readonly RebuildRunner rebuildRunner; - - public MigrationRebuilderHost(IApplicationLifetime lifetime, ISemanticLog log, RebuildRunner rebuildRunner) - : base(lifetime, log) - { - this.rebuildRunner = rebuildRunner; - } - - protected override Task StartAsync(ISemanticLog log, CancellationToken ct) - { - return rebuildRunner.RunAsync(ct); - } - } -} diff --git a/src/Squidex/Config/Startup/MigratorHost.cs b/src/Squidex/Config/Startup/MigratorHost.cs deleted file mode 100644 index 53ff2305e..000000000 --- a/src/Squidex/Config/Startup/MigratorHost.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Migrations; - -namespace Squidex.Config.Startup -{ - public sealed class MigratorHost : SafeHostedService - { - private readonly Migrator migrator; - - public MigratorHost(Migrator migrator, IApplicationLifetime lifetime, ISemanticLog log) - : base(lifetime, log) - { - this.migrator = migrator; - } - - protected override Task StartAsync(ISemanticLog log, CancellationToken ct) - { - return migrator.MigrateAsync(ct); - } - } -} diff --git a/src/Squidex/Config/Startup/SafeHostedService.cs b/src/Squidex/Config/Startup/SafeHostedService.cs deleted file mode 100644 index 90f39c691..000000000 --- a/src/Squidex/Config/Startup/SafeHostedService.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Config.Startup -{ - public abstract class SafeHostedService : IHostedService - { - private readonly IApplicationLifetime lifetime; - private readonly ISemanticLog log; - private bool isStarted; - - protected SafeHostedService(IApplicationLifetime lifetime, ISemanticLog log) - { - this.lifetime = lifetime; - - this.log = log; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - await StartAsync(log, cancellationToken); - - isStarted = true; - } - catch - { - lifetime.StopApplication(); - throw; - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - if (isStarted) - { - await StopAsync(log, cancellationToken); - } - } - - protected abstract Task StartAsync(ISemanticLog log, CancellationToken ct); - - protected virtual Task StopAsync(ISemanticLog log, CancellationToken ct) - { - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex/Config/Web/WebExtensions.cs b/src/Squidex/Config/Web/WebExtensions.cs deleted file mode 100644 index badab15c3..000000000 --- a/src/Squidex/Config/Web/WebExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Net.Http.Headers; -using Squidex.Infrastructure.Json; -using Squidex.Pipeline.Robots; -using Squidex.Web.Pipeline; - -namespace Squidex.Config.Web -{ - public static class WebExtensions - { - public static IApplicationBuilder UseMyLocalCache(this IApplicationBuilder app) - { - app.UseMiddleware(); - - return app; - } - - public static IApplicationBuilder UseMyTracking(this IApplicationBuilder app) - { - app.UseMiddleware(); - - return app; - } - - public static IApplicationBuilder UseMyHealthCheck(this IApplicationBuilder app) - { - var serializer = app.ApplicationServices.GetRequiredService(); - - var writer = new Func((httpContext, report) => - { - var response = new - { - Entries = report.Entries.ToDictionary(x => x.Key, x => - { - var value = x.Value; - - return new - { - Data = value.Data.Count > 0 ? new Dictionary(value.Data) : null, - value.Description, - value.Duration, - value.Status - }; - }), - report.Status, - report.TotalDuration - }; - - var json = serializer.Serialize(response); - - httpContext.Response.Headers[HeaderNames.ContentType] = "text/json"; - - return httpContext.Response.WriteAsync(json); - }); - - app.UseHealthChecks("/readiness", new HealthCheckOptions - { - Predicate = check => true, - ResponseWriter = writer - }); - - app.UseHealthChecks("/healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("node"), - ResponseWriter = writer - }); - - app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("cluster"), - ResponseWriter = writer - }); - - return app; - } - - public static IApplicationBuilder UseMyRobotsTxt(this IApplicationBuilder app) - { - app.Map("/robots.txt", builder => builder.UseMiddleware()); - - return app; - } - - public static void UseMyCors(this IApplicationBuilder app) - { - app.UseCors(builder => builder - .AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader()); - } - - public static void UseMyForwardingRules(this IApplicationBuilder app) - { - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedProto, - ForwardLimit = null, - RequireHeaderSymmetry = false - }); - - app.UseMiddleware(); - app.UseMiddleware(); - } - } -} diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs deleted file mode 100644 index 97ef2eb56..000000000 --- a/src/Squidex/Config/Web/WebServices.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Config.Domain; -using Squidex.Domain.Apps.Entities; -using Squidex.Pipeline.Plugins; -using Squidex.Pipeline.Robots; -using Squidex.Web; -using Squidex.Web.Pipeline; - -namespace Squidex.Config.Web -{ - public static class WebServices - { - public static void AddMyMvcWithPlugins(this IServiceCollection services, IConfiguration config) - { - services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config, typeof(WebServices).Assembly)) - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsOptional(); - - services.Configure(options => - { - options.SuppressModelStateInvalidFilter = true; - }); - - services.AddMvc(options => - { - options.Filters.Add(); - options.Filters.Add(); - options.Filters.Add(); - options.Filters.Add(); - }) - .AddMyPlugins(config) - .AddMySerializers(); - - services.AddCors(); - services.AddRouting(); - } - } -} diff --git a/src/Squidex/Dockerfile b/src/Squidex/Dockerfile deleted file mode 100644 index fc00bfa8c..000000000 --- a/src/Squidex/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM microsoft/dotnet:2.2.0-aspnetcore-runtime - -WORKDIR /app - -# Copy from current directory -COPY . . - -EXPOSE 80 -EXPOSE 33333 -EXPOSE 40000 - -ENTRYPOINT ["dotnet", "Squidex.dll"] \ No newline at end of file diff --git a/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs b/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs deleted file mode 100644 index 1d4a3dcb0..000000000 --- a/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ========================================================================== -// 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.IO; -using Microsoft.AspNetCore.Http; -using NJsonSchema; -using NSwag; - -namespace Squidex.Pipeline.OpenApi -{ - public static class NSwagHelper - { - public static readonly string SecurityDocs = LoadDocs("security"); - - public static readonly string SchemaBodyDocs = LoadDocs("schemabody"); - - public static readonly string SchemaQueryDocs = LoadDocs("schemaquery"); - - private static string LoadDocs(string name) - { - var assembly = typeof(NSwagHelper).Assembly; - - using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) - { - using (var streamReader = new StreamReader(resourceStream)) - { - return streamReader.ReadToEnd(); - } - } - } - - public static OpenApiDocument CreateApiDocument(HttpContext context, string appName) - { - var scheme = - string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? - OpenApiSchema.Http : - OpenApiSchema.Https; - - var document = new OpenApiDocument - { - Schemes = new List - { - scheme - }, - Consumes = new List - { - "application/json" - }, - Produces = new List - { - "application/json" - }, - Info = new OpenApiInfo - { - Title = $"Squidex API for {appName} App" - }, - SchemaType = SchemaType.OpenApi3 - }; - - if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) - { - document.Host = context.Request.Host.Value; - } - - return document; - } - - public static void AddQuery(this OpenApiOperation operation, string name, JsonObjectType type, string description) - { - var schema = new JsonSchema { Type = type }; - - operation.AddParameter(name, schema, OpenApiParameterKind.Query, description, false); - } - - public static void AddPathParameter(this OpenApiOperation operation, string name, JsonObjectType type, string description, string format = null) - { - var schema = new JsonSchema { Type = type, Format = format }; - - operation.AddParameter(name, schema, OpenApiParameterKind.Path, description, true); - } - - public static void AddBody(this OpenApiOperation operation, string name, JsonSchema schema, string description) - { - operation.AddParameter(name, schema, OpenApiParameterKind.Body, description, true); - } - - private static void AddParameter(this OpenApiOperation operation, string name, JsonSchema schema, OpenApiParameterKind kind, string description, bool isRequired) - { - var parameter = new OpenApiParameter { Schema = schema, Name = name, Kind = kind }; - - if (!string.IsNullOrWhiteSpace(description)) - { - parameter.Description = description; - } - - parameter.IsRequired = isRequired; - - operation.Parameters.Add(parameter); - } - - public static void AddResponse(this OpenApiOperation operation, string statusCode, string description, JsonSchema schema = null) - { - var response = new OpenApiResponse { Description = description, Schema = schema }; - - operation.Responses.Add(statusCode, response); - } - } -} diff --git a/src/Squidex/Pipeline/Plugins/PluginExtensions.cs b/src/Squidex/Pipeline/Plugins/PluginExtensions.cs deleted file mode 100644 index 4608a8659..000000000 --- a/src/Squidex/Pipeline/Plugins/PluginExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Plugins; - -namespace Squidex.Pipeline.Plugins -{ - public static class PluginExtensions - { - public static IMvcBuilder AddMyPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) - { - var pluginManager = new PluginManager(); - - var options = config.Get(); - - if (options.Plugins != null) - { - foreach (var path in options.Plugins) - { - var plugin = PluginLoaders.LoadPlugin(path); - - if (plugin != null) - { - try - { - var pluginAssembly = plugin.LoadDefaultAssembly(); - - pluginAssembly.AddParts(mvcBuilder); - pluginManager.Add(path, pluginAssembly); - } - catch (Exception ex) - { - pluginManager.LogException(path, "LoadingAssembly", ex); - } - } - else - { - pluginManager.LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); - } - } - } - - pluginManager.ConfigureServices(mvcBuilder.Services, config); - - mvcBuilder.Services.AddSingleton(pluginManager); - - return mvcBuilder; - } - - public static void UsePluginsBefore(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService(); - - pluginManager.ConfigureBefore(app); - } - - public static void UsePluginsAfter(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService(); - - pluginManager.ConfigureAfter(app); - } - - public static void UsePlugins(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService(); - - pluginManager.Log(app.ApplicationServices.GetService()); - } - } -} diff --git a/src/Squidex/Pipeline/Plugins/PluginLoaders.cs b/src/Squidex/Pipeline/Plugins/PluginLoaders.cs deleted file mode 100644 index 5cc49e040..000000000 --- a/src/Squidex/Pipeline/Plugins/PluginLoaders.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using McMaster.NETCore.Plugins; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Plugins; -using Squidex.Web; - -namespace Squidex.Pipeline.Plugins -{ - public static class PluginLoaders - { - private static readonly Type[] SharedTypes = - { - typeof(IPlugin), - typeof(SquidexCoreModel), - typeof(SquidexCoreOperations), - typeof(SquidexEntities), - typeof(SquidexEvents), - typeof(SquidexInfrastructure), - typeof(SquidexWeb) - }; - - public static PluginLoader LoadPlugin(string pluginPath) - { - foreach (var candidate in GetPaths(pluginPath)) - { - if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) - { - return PluginLoader.CreateFromAssemblyFile(candidate.FullName, PluginLoaderOptions.PreferSharedTypes); - } - - if (candidate.Extension.Equals(".json", StringComparison.OrdinalIgnoreCase)) - { - return PluginLoader.CreateFromConfigFile(candidate.FullName, SharedTypes); - } - } - - return null; - } - - private static IEnumerable GetPaths(string pluginPath) - { - var candidate = new FileInfo(Path.GetFullPath(pluginPath)); - - if (candidate.Exists) - { - yield return candidate; - } - - if (!Path.IsPathRooted(pluginPath)) - { - candidate = new FileInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), pluginPath)); - - if (candidate.Exists) - { - yield return candidate; - } - } - } - } -} diff --git a/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs deleted file mode 100644 index 92ed64692..000000000 --- a/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure; - -namespace Squidex.Pipeline.Robots -{ - public sealed class RobotsTxtMiddleware : IMiddleware - { - private readonly RobotsTxtOptions robotsTxtOptions; - - public RobotsTxtMiddleware(IOptions robotsTxtOptions) - { - Guard.NotNull(robotsTxtOptions, nameof(robotsTxtOptions)); - - this.robotsTxtOptions = robotsTxtOptions.Value; - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(robotsTxtOptions.Text)) - { - context.Response.ContentType = "text/plain"; - context.Response.StatusCode = 200; - - await context.Response.WriteAsync(robotsTxtOptions.Text); - } - else - { - await next(context); - } - } - - private static bool CanServeRequest(HttpRequest request) - { - return HttpMethods.IsGet(request.Method) && string.IsNullOrEmpty(request.Path); - } - } -} diff --git a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs deleted file mode 100644 index a9f2e305a..000000000 --- a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Squidex.Pipeline.Squid -{ - public sealed class SquidMiddleware - { - private readonly RequestDelegate next; - private readonly string squidHappyLG = LoadSvg("happy"); - private readonly string squidHappySM = LoadSvg("happy-sm"); - private readonly string squidSadLG = LoadSvg("sad"); - private readonly string squidSadSM = LoadSvg("sad-sm"); - - public SquidMiddleware(RequestDelegate next) - { - this.next = next; - } - - public async Task Invoke(HttpContext context) - { - var request = context.Request; - - if (request.Path.Equals("/squid.svg")) - { - var face = "sad"; - - if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) - { - face = faceValue; - } - - var isSad = face == "sad"; - - var title = isSad ? "OH DAMN!" : "OH YEAH!"; - - if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) - { - title = titleValue; - } - - var text = "text"; - - if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) - { - text = textValue; - } - - var background = isSad ? "#F5F5F9" : "#4CC159"; - - if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) - { - background = backgroundValue; - } - - var isSmall = request.Query.TryGetValue("small", out _); - - string svg; - - if (isSmall) - { - svg = isSad ? squidSadSM : squidHappySM; - } - else - { - svg = isSad ? squidSadLG : squidHappyLG; - } - - var (l1, l2, l3) = SplitText(text); - - svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); - svg = svg.Replace("{{TEXT1}}", l1); - svg = svg.Replace("{{TEXT2}}", l2); - svg = svg.Replace("{{TEXT3}}", l3); - svg = svg.Replace("[COLOR]", background); - - context.Response.StatusCode = 200; - context.Response.ContentType = "image/svg+xml"; - context.Response.Headers["Cache-Control"] = "public, max-age=604800"; - - await context.Response.WriteAsync(svg); - } - else - { - await next(context); - } - } - - private static (string, string, string) SplitText(string text) - { - var result = new List(); - - var line = new StringBuilder(); - - foreach (var word in text.Split(' ')) - { - if (line.Length + word.Length > 17 && line.Length > 0) - { - result.Add(line.ToString()); - - line.Clear(); - } - - if (line.Length > 0) - { - line.Append(" "); - } - - line.Append(word); - } - - result.Add(line.ToString()); - - while (result.Count < 3) - { - result.Add(string.Empty); - } - - return (result[0], result[1], result[2]); - } - - private static string LoadSvg(string name) - { - var assembly = typeof(SquidMiddleware).Assembly; - - using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) - { - using (var streamReader = new StreamReader(resourceStream)) - { - return streamReader.ReadToEnd(); - } - } - } - } -} diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs deleted file mode 100644 index 2d11f10c2..000000000 --- a/src/Squidex/Program.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Squidex.Config; -using Squidex.Infrastructure.Log.Adapter; - -namespace Squidex -{ - public static class Program - { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - new WebHostBuilder() - .UseKestrel(k => { k.AddServerHeader = false; }) - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIIS() - .UseStartup() - .ConfigureLogging((hostingContext, builder) => - { - builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); - builder.AddSemanticLog(); - builder.AddFilters(); - }) - .ConfigureAppConfiguration((hostContext, builder) => - { - builder.Sources.Clear(); - - builder.AddJsonFile($"appsettings.json", true); - builder.AddJsonFile($"appsettings.Custom.json", true); - builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true); - - builder.AddEnvironmentVariables(); - - builder.AddCommandLine(args); - }) - .Build(); - } -} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj deleted file mode 100644 index aabd271cc..000000000 --- a/src/Squidex/Squidex.csproj +++ /dev/null @@ -1,157 +0,0 @@ - - - InProcess - true - 2.2.0 - netcoreapp2.2 - Latest - true - 7.3 - - - - full - True - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_DocumentationFile Include="$(DocumentationFile)" /> - - - - - - true - - - - ..\..\Squidex.ruleset - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060 - - \ No newline at end of file diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs deleted file mode 100644 index b17cb7ea7..000000000 --- a/src/Squidex/WebStartup.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Migrate_01; -using Squidex.Areas.Api; -using Squidex.Areas.Api.Config.OpenApi; -using Squidex.Areas.Api.Controllers.Contents; -using Squidex.Areas.Api.Controllers.News; -using Squidex.Areas.Api.Controllers.UI; -using Squidex.Areas.Frontend; -using Squidex.Areas.IdentityServer; -using Squidex.Areas.IdentityServer.Config; -using Squidex.Areas.OrleansDashboard; -using Squidex.Areas.Portal; -using Squidex.Config; -using Squidex.Config.Authentication; -using Squidex.Config.Domain; -using Squidex.Config.Orleans; -using Squidex.Config.Startup; -using Squidex.Config.Web; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.Translations; -using Squidex.Pipeline.Plugins; -using Squidex.Pipeline.Robots; -using Squidex.Web; -using Squidex.Web.Pipeline; - -namespace Squidex -{ - public sealed class WebStartup - { - private readonly IConfiguration config; - private readonly IHostingEnvironment environment; - - public WebStartup(IConfiguration config, IHostingEnvironment environment) - { - this.config = config; - - this.environment = environment; - } - - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddHttpClient(); - services.AddLogging(); - services.AddMemoryCache(); - services.AddOptions(); - - services.AddMyMvcWithPlugins(config); - - services.AddMyAssetServices(config); - services.AddMyAuthentication(config); - services.AddMyEntitiesServices(config); - services.AddMyEventPublishersServices(config); - services.AddMyEventStoreServices(config); - services.AddMyIdentityServer(); - services.AddMyInfrastructureServices(config); - services.AddMyLoggingServices(config); - services.AddMyOpenApiSettings(); - services.AddMyMigrationServices(); - services.AddMyRuleServices(); - services.AddMySerializers(); - services.AddMyStoreServices(config); - services.AddMySubscriptionServices(config); - - services.Configure( - config.GetSection("contents")); - services.Configure( - config.GetSection("assets")); - services.Configure( - config.GetSection("translations:deepL")); - services.Configure( - config.GetSection("languages")); - services.Configure( - config.GetSection("mode")); - services.Configure( - config.GetSection("robots")); - services.Configure( - config.GetSection("healthz:gc")); - services.Configure( - config.GetSection("etags")); - services.Configure( - config.GetSection("urls")); - services.Configure( - config.GetSection("usage")); - services.Configure( - config.GetSection("rebuild")); - services.Configure( - config.GetSection("exposedConfiguration")); - services.Configure( - config.GetSection("rules")); - - services.Configure( - config.GetSection("contentsController")); - services.Configure( - config.GetSection("identity")); - services.Configure( - config.GetSection("ui")); - services.Configure( - config.GetSection("news")); - - services.AddHostedService(); - - services.AddOrleans(config, environment); - - services.AddHostedService(); - services.AddHostedService(); - services.AddHostedService(); - - return services.BuildServiceProvider(); - } - - public void Configure(IApplicationBuilder app) - { - app.ApplicationServices.LogConfiguration(); - - app.UsePluginsBefore(); - - app.UseMyHealthCheck(); - app.UseMyRobotsTxt(); - app.UseMyTracking(); - app.UseMyLocalCache(); - app.UseMyCors(); - app.UseMyForwardingRules(); - - app.ConfigureApi(); - app.ConfigurePortal(); - app.ConfigureOrleansDashboard(); - app.ConfigureIdentityServer(); - app.ConfigureFrontend(); - - app.UsePluginsAfter(); - app.UsePlugins(); - } - } -} diff --git a/src/Squidex/app-config/webpack.config.js b/src/Squidex/app-config/webpack.config.js deleted file mode 100644 index d960816dc..000000000 --- a/src/Squidex/app-config/webpack.config.js +++ /dev/null @@ -1,376 +0,0 @@ -const webpack = require('webpack'), - path = require('path'); - -const appRoot = path.resolve(__dirname, '..'); - -function root() { - var newArgs = Array.prototype.slice.call(arguments, 0); - - return path.join.apply(path, [appRoot].concat(newArgs)); -}; - -const plugins = { - // https://github.com/webpack-contrib/mini-css-extract-plugin - MiniCssExtractPlugin: require('mini-css-extract-plugin'), - // https://github.com/dividab/tsconfig-paths-webpack-plugin - TsconfigPathsPlugin: require('tsconfig-paths-webpack-plugin'), - // https://github.com/aackerman/circular-dependency-plugin - CircularDependencyPlugin: require('circular-dependency-plugin'), - // https://github.com/jantimon/html-webpack-plugin - HtmlWebpackPlugin: require('html-webpack-plugin'), - // https://webpack.js.org/plugins/terser-webpack-plugin/ - TerserPlugin: require('terser-webpack-plugin'), - // https://www.npmjs.com/package/@ngtools/webpack - NgToolsWebpack: require('@ngtools/webpack'), - // https://github.com/NMFR/optimize-css-assets-webpack-plugin - OptimizeCSSAssetsPlugin: require("optimize-css-assets-webpack-plugin"), - // https://github.com/jrparish/tslint-webpack-plugin - TsLintPlugin: require('tslint-webpack-plugin') -}; - -module.exports = function (env) { - const isDevServer = path.basename(require.main.filename) === 'webpack-dev-server.js'; - const isProduction = env && env.production; - const isTests = env && env.target === 'tests'; - const isCoverage = env && env.coverage; - const isAot = isProduction; - - const config = { - mode: isProduction ? 'production' : 'development', - - /** - * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack. - * - * See: https://webpack.js.org/configuration/devtool/ - */ - devtool: isProduction ? false : 'inline-source-map', - - /** - * Options affecting the resolving of modules. - * - * See: https://webpack.js.org/configuration/resolve/ - */ - resolve: { - /** - * An array of extensions that should be used to resolve modules. - * - * See: https://webpack.js.org/configuration/resolve/#resolve-extensions - */ - extensions: ['.ts', '.js', '.mjs', '.css', '.scss'], - modules: [ - root('app'), - root('app', 'theme'), - root('node_modules') - ], - - plugins: [ - new plugins.TsconfigPathsPlugin() - ] - }, - - /** - * Options affecting the normal modules. - * - * See: https://webpack.js.org/configuration/module/ - */ - module: { - /** - * An array of Rules which are matched to requests when modules are created. - * - * See: https://webpack.js.org/configuration/module/#module-rules - */ - rules: [{ - test: /\.mjs$/, - type: "javascript/auto", - include: [/node_modules/] - }, { - test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, - parser: { system: true }, - include: [/node_modules/] - }, { - test: /\.js\.flow$/, - use: [{ - loader: 'ignore-loader' - }], - include: [/node_modules/] - }, { - test: /\.map$/, - use: [{ - loader: 'ignore-loader' - }], - include: [/node_modules/] - }, { - test: /\.d\.ts$/, - use: [{ - loader: 'ignore-loader' - }], - include: [/node_modules/] - }, { - test: /\.(woff|woff2|ttf|eot)(\?.*$|$)/, - use: [{ - loader: 'file-loader?name=[name].[hash].[ext]', - options: { - outputPath: 'assets', - /* - * Use custom public path as ./ is not supported by fonts. - */ - publicPath: isDevServer ? undefined : 'assets' - } - }] - }, { - test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/, - use: [{ - loader: 'file-loader?name=[name].[hash].[ext]', - options: { - outputPath: 'assets' - } - }] - }, { - test: /\.css$/, - use: [ - plugins.MiniCssExtractPlugin.loader, - { - loader: 'css-loader' - }] - }, { - test: /\.scss$/, - use: [{ - loader: 'raw-loader' - }, { - loader: 'sass-loader', options: { - sassOptions: { - includePaths: [root('app', 'theme')] - } - } - }], - exclude: root('app', 'theme') - }] - }, - - plugins: [ - new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, root('./app'), {}), - new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/), - - /** - * Puts each bundle into a file and appends the hash of the file to the path. - * - * See: https://github.com/webpack-contrib/mini-css-extract-plugin - */ - new plugins.MiniCssExtractPlugin('[name].css'), - - new webpack.LoaderOptionsPlugin({ - options: { - htmlLoader: { - /** - * Define the root for images, so that we can use absolute urls. - * - * See: https://github.com/webpack/html-loader#Advanced_Options - */ - root: root('app', 'images') - }, - context: '/' - } - }), - - /** - * Detect circular dependencies in app. - * - * See: https://github.com/aackerman/circular-dependency-plugin - */ - new plugins.CircularDependencyPlugin({ - exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/, - // Add errors to webpack instead of warnings - failOnError: true - }), - ], - - devServer: { - headers: { - 'Access-Control-Allow-Origin': '*' - }, - historyApiFallback: true - } - }; - - if (!isTests) { - /** - * The entry point for the bundle. Our Angular app. - * - * See: https://webpack.js.org/configuration/entry-context/ - */ - config.entry = { - 'shims': './app/shims.ts', - 'app': './app/app.ts' - }; - - if (isProduction) { - config.output = { - /** - * The output directory as absolute path (required). - * - * See: https://webpack.js.org/configuration/output/#output-path - */ - path: root('wwwroot/build/'), - - publicPath: './build/', - - /** - * Specifies the name of each output file on disk. - * - * See: https://webpack.js.org/configuration/output/#output-filename - */ - filename: '[name].js', - - /** - * The filename of non-entry chunks as relative path inside the output.path directory. - * - * See: https://webpack.js.org/configuration/output/#output-chunkfilename - */ - chunkFilename: '[id].[hash].chunk.js' - }; - } else { - config.output = { - filename: '[name].js', - - /** - * Set the public path, because we are running the website from another port (5000). - */ - publicPath: 'http://localhost:3000/' - }; - } - - config.plugins.push( - new plugins.HtmlWebpackPlugin({ - hash: true, - chunks: ['shims', 'app'], - chunksSortMode: 'manual', - template: 'wwwroot/index.html' - }) - ); - - config.plugins.push( - new plugins.HtmlWebpackPlugin({ - template: 'wwwroot/_theme.html', hash: true, chunksSortMode: 'none', filename: 'theme.html' - }) - ); - - config.plugins.push( - new plugins.TsLintPlugin({ - files: ['./app/**/*.ts'], - /** - * Path to a configuration file. - */ - config: root('tslint.json'), - /** - * Wait for linting and fail the build when linting error occur. - */ - waitForLinting: isProduction - }) - ); - } - - if (!isCoverage) { - config.plugins.push( - new plugins.NgToolsWebpack.AngularCompilerPlugin({ - directTemplateLoading: true, - entryModule: 'app/app.module#AppModule', - sourceMap: !isProduction, - skipCodeGeneration: !isAot, - tsConfigPath: './tsconfig.json' - }) - ); - } - - if (isProduction) { - config.optimization = { - minimizer: [ - new plugins.TerserPlugin({ - terserOptions: { - compress: true, - ecma: 5, - mangle: true, - output: { - comments: false - }, - safari10: true - }, - extractComments: true - }), - - new plugins.OptimizeCSSAssetsPlugin({}) - ] - }; - - config.performance = { - hints: false - }; - } - - if (isCoverage) { - // Do not instrument tests. - config.module.rules.push({ - test: /\.ts$/, - use: [{ - loader: 'ts-loader' - }], - include: [/\.(e2e|spec)\.ts$/], - }); - - // Use instrument loader for all normal files. - config.module.rules.push({ - test: /\.ts$/, - use: [{ - loader: 'istanbul-instrumenter-loader?esModules=true' - }, { - loader: 'ts-loader' - }], - exclude: [/\.(e2e|spec)\.ts$/] - }); - } else { - config.module.rules.push({ - test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, - use: [{ - loader: plugins.NgToolsWebpack.NgToolsLoader - }] - }) - } - - if (isProduction) { - config.module.rules.push({ - test: /\.scss$/, - /* - * Extract the content from a bundle to a file. - * - * See: https://github.com/webpack-contrib/extract-text-webpack-plugin - */ - use: [ - plugins.MiniCssExtractPlugin.loader, - { - loader: 'css-loader' - }, { - loader: 'sass-loader' - }], - /* - * Do not include component styles. - */ - include: root('app', 'theme'), - }); - } else { - config.module.rules.push({ - test: /\.scss$/, - use: [{ - loader: 'style-loader' - }, { - loader: 'css-loader' - }, { - loader: 'sass-loader?sourceMap' - }], - /* - * Do not include component styles. - */ - include: root('app', 'theme') - }); - } - - return config; -}; \ No newline at end of file diff --git a/src/Squidex/package-lock.json b/src/Squidex/package-lock.json deleted file mode 100644 index e2495cb60..000000000 --- a/src/Squidex/package-lock.json +++ /dev/null @@ -1,17154 +0,0 @@ -{ - "name": "squidex", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@angular-devkit/build-optimizer": { - "version": "0.803.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.803.8.tgz", - "integrity": "sha512-UiMxl1wI3acqIoRkC0WA0qpab+ni6SlCaB4UIwfD1H/FdzU80P04AIUuJS7StxjbwVkVtA05kcfgmqzP8yBMVg==", - "dev": true, - "requires": { - "loader-utils": "1.2.3", - "source-map": "0.7.3", - "tslib": "1.10.0", - "typescript": "3.5.3", - "webpack-sources": "1.4.3" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - } - } - }, - "@angular-devkit/core": { - "version": "8.3.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.8.tgz", - "integrity": "sha512-HwlMRr6qANwhOJS+5rGgQ2lmP4nj2C4cbUc0LlA09Cdbq0RnDquUFVqHF6h81FUKFW1D5qDehWYHNOVq8+gTkQ==", - "dev": true, - "requires": { - "ajv": "6.10.2", - "fast-json-stable-stringify": "2.0.0", - "magic-string": "0.25.3", - "rxjs": "6.4.0", - "source-map": "0.7.3" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "magic-string": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", - "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "@angular/animations": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-8.2.9.tgz", - "integrity": "sha512-l30AF0d9P5okTPM1wieUHgcnDyGSNvyaBcxXSOkT790wAP2v5zs7VrKq9Lm+ICu4Nkx07KrOr5XLUHhqsg3VXA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/cdk": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.3.tgz", - "integrity": "sha512-ZwO5Sn720RA2YvBqud0JAHkZXjmjxM0yNzCO8RVtRE9i8Gl26Wk0j0nQeJkVm4zwv2QO8MwbKUKGTMt8evsokA==", - "requires": { - "parse5": "^5.0.0", - "tslib": "^1.7.1" - } - }, - "@angular/common": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-8.2.9.tgz", - "integrity": "sha512-76WDU1USlI5vAzqCJ3gxCQGuu57aJEggNk/xoWmQEXipiFTFBh2wSKn/dE6Txr/q3COTPIcrmb9OCeal5kQPIA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/compiler": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.9.tgz", - "integrity": "sha512-oQho19DnOhEDNerCOGuGK95tcZ2oy4dSA5SykJmmniRnZzPM2++bJD32qJehXHy1K+3hv2zN9x7HPhqT3ljT6g==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/compiler-cli": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.9.tgz", - "integrity": "sha512-tqGBKPf3SRYNEGGJbmjom//U/eAjnecDhGUw6o+VkYE/wxYd9pPcLmcEwwyXBpIPJAsN8RsjTikPuH0gcNE8bw==", - "dev": true, - "requires": { - "canonical-path": "1.0.0", - "chokidar": "^2.1.1", - "convert-source-map": "^1.5.1", - "dependency-graph": "^0.7.2", - "magic-string": "^0.25.0", - "minimist": "^1.2.0", - "reflect-metadata": "^0.1.2", - "source-map": "^0.6.1", - "tslib": "^1.9.0", - "yargs": "13.1.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - } - } - }, - "@angular/core": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-8.2.9.tgz", - "integrity": "sha512-GpHAuLOlN9iioELCQBmAsjETTUCyFgVUI3LXwh3e63jnpd+ZuuZcZbjfTYhtgYVNMetn7cVEO6p88eb7qvpUWQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/forms": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-8.2.9.tgz", - "integrity": "sha512-kAdBuApC9PPOdPI8BmNhxCraAkXGbX/PkVan8pQ5xdumvgGqvVjbJvLaUSbJROPtgCRlQyiEDrHFd4gk/WU76A==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/http": { - "version": "7.2.15", - "resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.15.tgz", - "integrity": "sha512-TR7PEdmLWNIre3Zn8lvyb4lSrvPUJhKLystLnp4hBMcWsJqq5iK8S3bnlR4viZ9HMlf7bW7+Hm4SI6aB3tdUtw==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/platform-browser": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.9.tgz", - "integrity": "sha512-k3aNZy0OTqGn7HlHHV52QF6ZAP/VlQhWGD2u5e1dWIWMq39kdkdSCNu5tiuAf5hIzMBiSQ0tjnuVWA4MuDBYIQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/platform-browser-dynamic": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.9.tgz", - "integrity": "sha512-GbE4TUy4n/a8yp8fLWwdG/QnjUPZZ8VufItZ7GvOpoyknzegvka111dLctvMoPzSAsrKyShL6cryuyDC5PShUA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/platform-server": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-8.2.9.tgz", - "integrity": "sha512-rr6h82+DdUGhpsF3WT3eLk5itjZDXe7SiNtRGHkPj+yTyFAxuTKA3cX0N7LWsGGIFax+s1vQhMreV4YcyHKGPQ==", - "requires": { - "domino": "^2.1.2", - "tslib": "^1.9.0", - "xhr2": "^0.1.4" - } - }, - "@angular/router": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-8.2.9.tgz", - "integrity": "sha512-4P60CWNB/jxGjDBEuYN0Jobt76QlebAQeFBTDswRVwRlq/WJT4QhL3a8AVIRsHn9bQII0LUt/ZQBBPxn7h9lSA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/generator": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", - "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", - "dev": true, - "requires": { - "@babel/types": "^7.5.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", - "dev": true - }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/traverse": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", - "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.5", - "@babel/types": "^7.5.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", - "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - } - } - }, - "@ngtools/webpack": { - "version": "8.3.8", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.3.8.tgz", - "integrity": "sha512-jLN4/Abue+Ro/K2SF0TpHOXnFHGuaHQ4aL6QG++moZXavBxRdc2E+PDjtuaMaS1llLHs5C5GX+Ve9ueEFhWoeQ==", - "dev": true, - "requires": { - "@angular-devkit/core": "8.3.8", - "enhanced-resolve": "4.1.0", - "rxjs": "6.4.0", - "tree-kill": "1.2.1", - "webpack-sources": "1.4.3" - }, - "dependencies": { - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "@types/core-js": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.2.tgz", - "integrity": "sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/jasmine": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.2.tgz", - "integrity": "sha512-SaSSGOzwUnBEn64c+HTyVTJhRf8F1CXZLnxYx2ww3UrgGBmEEw38RSux2l3fYiT9brVLP67DU5omWA6V9OHI5Q==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "@types/marked": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.6.5.tgz", - "integrity": "sha512-6kBKf64aVfx93UJrcyEZ+OBM5nGv4RLsI6sR1Ar34bpgvGVRoyTgpxn4ZmtxOM5aDTAaaznYuYUH8bUX3Nk3YA==", - "dev": true - }, - "@types/mersenne-twister": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/mersenne-twister/-/mersenne-twister-1.1.2.tgz", - "integrity": "sha512-7KMIfSkMpaVExbzJRLUXHMO4hkFWbbspHPREk8I6pBxiNN+3+l6eAEClMCIPIo2KjCkR0rjYfXppr6+wKdTwpA==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mousetrap": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.0.tgz", - "integrity": "sha512-Jn2cF8X6RAMiSmJaATGjf2r3GzIfpZQpvnQhKprQ5sAbMaNXc7hc9sA2XHdMl3bEMEQhTV79JVW7n4Pgg7sjtg==", - "dev": true - }, - "@types/node": { - "version": "12.7.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz", - "integrity": "sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw==", - "dev": true - }, - "@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true - }, - "@types/q": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", - "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", - "dev": true - }, - "@types/react": { - "version": "16.9.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", - "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "csstype": "^2.2.0" - } - }, - "@types/react-dom": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz", - "integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/sortablejs": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.7.2.tgz", - "integrity": "sha512-yIxpbtlfhaFi2QyuUK54XcmzDWZf5i11CgTrMO4Vh+sKKZthonizkTcqhADeHdngDNTDVUCYfIcfIvpZRAZY+A==", - "dev": true - }, - "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - } - } - }, - "ajv": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", - "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", - "dev": true, - "requires": { - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0", - "uri-js": "^3.0.2" - } - }, - "ajv-errors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", - "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", - "dev": true - }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "angular2-chartjs": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/angular2-chartjs/-/angular2-chartjs-0.5.1.tgz", - "integrity": "sha512-bxEVxVEv7llMcgwuc9jlc5KmuOEngT7ZlUyCddmsXwQQAahrTeNgFJ1Nc1SVQnq2fl2d8efh6m70DqF5beiA+A==", - "requires": { - "chart.js": "^2.3.0" - } - }, - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - }, - "dependencies": { - "color-convert": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "dev": true, - "requires": { - "color-name": "1.1.1" - } - }, - "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true - } - } - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } - }, - "app-root-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", - "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==", - "dev": true - }, - "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", - "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - } - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "axobject-query": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", - "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - } - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true - }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", - "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==", - "dev": true - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "bootstrap": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", - "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", - "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000989", - "electron-to-chromium": "^1.3.247", - "node-releases": "^1.1.29" - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - }, - "cacache": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", - "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", - "dev": true, - "requires": { - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "minipass": "^3.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", - "ssri": "^7.0.0", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30000998", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000998.tgz", - "integrity": "sha512-8Tj5sPZR9kMHeDD9SZXIVr5m9ofufLLCG2Y4QwQrH18GIwG+kCc+zYdlR036ZRkuKjVVetyxeAgGA1xF7XdmzQ==", - "dev": true - }, - "canonical-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", - "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chart.js": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.2.tgz", - "integrity": "sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA==", - "requires": { - "chartjs-color": "^2.1.0", - "moment": "^2.10.2" - } - }, - "chartjs-color": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz", - "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=", - "requires": { - "chartjs-color-string": "^0.5.0", - "color-convert": "^0.5.3" - } - }, - "chartjs-color-string": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz", - "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==", - "requires": { - "color-name": "^1.0.0" - } - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - } - }, - "chownr": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular-dependency-plugin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz", - "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==", - "dev": true - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", - "dev": true, - "requires": { - "source-map": "0.5.x" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "codelyzer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-5.1.2.tgz", - "integrity": "sha512-1z7mtpwxcz5uUqq0HLO0ifj/tz2dWEmeaK+8c5TEZXAwwVxrjjg0118ODCOCCOcpfYaaEHxStNCaWVYo9FUPXw==", - "dev": true, - "requires": { - "app-root-path": "^2.2.1", - "aria-query": "^3.0.0", - "axobject-query": "^2.0.2", - "css-selector-tokenizer": "^0.7.1", - "cssauron": "^1.4.0", - "damerau-levenshtein": "^1.0.4", - "semver-dsl": "^1.0.1", - "source-map": "^0.5.7", - "sprintf-js": "^1.1.2" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - } - } - }, - "codemirror": { - "version": "5.49.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.0.tgz", - "integrity": "sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==" - }, - "codemirror-graphql": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.8.3.tgz", - "integrity": "sha512-ZipSnPXFKDMThfvfTKTAt1dQmuGctVNann8hTZg6017+vwOcGpIqCuQIZLRDw/Y3zZfCyydRARHgbSydSCXpow==", - "requires": { - "graphql-language-service-interface": "^1.3.2", - "graphql-language-service-parser": "^1.2.2" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", - "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", - "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - }, - "dependencies": { - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - } - } - }, - "color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", - "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-versions": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz", - "integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==", - "dev": true - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, - "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", - "dev": true, - "requires": { - "mime-db": ">= 1.40.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - } - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-to-clipboard": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", - "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", - "requires": { - "toggle-selection": "^1.0.6" - } - }, - "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "cpx": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", - "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=", - "dev": true, - "requires": { - "babel-runtime": "^6.9.2", - "chokidar": "^1.6.0", - "duplexer": "^0.1.1", - "glob": "^7.0.5", - "glob2base": "^0.0.12", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "resolve": "^1.1.7", - "safe-buffer": "^5.0.1", - "shell-quote": "^1.6.1", - "subarg": "^1.0.0" - } - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-fetch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz", - "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=", - "requires": { - "node-fetch": "2.1.2", - "whatwg-fetch": "2.0.4" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "crypto-js": { - "version": "3.1.9-1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", - "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - } - }, - "css-loader": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.2.0.tgz", - "integrity": "sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.17", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.2", - "postcss-modules-scope": "^2.1.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.0.0", - "schema-utils": "^2.0.0" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "schema-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", - "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-selector-tokenizer": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", - "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", - "dev": true, - "requires": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" - } - }, - "css-tree": { - "version": "1.0.0-alpha.33", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz", - "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==", - "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.5.3" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "css-unit-converter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", - "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", - "dev": true - }, - "css-what": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", - "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", - "dev": true - }, - "cssauron": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", - "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", - "dev": true, - "requires": { - "through": "X.X.X" - } - }, - "cssesc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", - "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", - "dev": true - }, - "cssnano": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", - "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.7", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - } - }, - "cssnano-preset-default": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", - "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.2", - "postcss-unique-selectors": "^4.0.1" - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", - "dev": true - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true - }, - "csso": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", - "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", - "dev": true, - "requires": { - "css-tree": "1.0.0-alpha.29" - }, - "dependencies": { - "css-tree": { - "version": "1.0.0-alpha.29", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", - "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", - "dev": true, - "requires": { - "mdn-data": "~1.1.0", - "source-map": "^0.5.3" - } - }, - "mdn-data": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", - "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "csstype": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", - "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", - "dev": true - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", - "dev": true - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "damerau-levenshtein": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", - "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", - "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", - "dev": true - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", - "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", - "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", - "dev": true - }, - "default-gateway": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", - "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "ip-regex": "^2.1.0" - } - }, - "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "dependency-graph": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz", - "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true - }, - "di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", - "dev": true - }, - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "dom-converter": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", - "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", - "dev": true, - "requires": { - "utila": "~0.3" - }, - "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } - } - }, - "dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", - "dev": true, - "requires": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", - "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domino": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.3.tgz", - "integrity": "sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg==" - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.273", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.273.tgz", - "integrity": "sha512-0kUppiHQvHEENHh+nTtvTt4eXMwcPyWmMaj73GPrSEm3ldKhmmHuOH6IjrmuW6YmyS/fpXcLvMQLNVpqRhpNWw==", - "dev": true - }, - "elliptic": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", - "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "engine.io": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", - "dev": true, - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.5", - "has-binary2": "~1.0.2" - } - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", - "dev": true - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "dev": true, - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, - "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" - } - }, - "es5-ext": { - "version": "0.10.46", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", - "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-templates": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", - "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", - "dev": true, - "requires": { - "recast": "~0.11.12", - "through": "~2.3.6" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", - "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "concat-stream": "^1.4.6", - "debug": "^2.1.1", - "doctrine": "^1.2.2", - "es6-map": "^0.1.3", - "escope": "^3.6.0", - "espree": "^3.1.6", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^1.1.1", - "glob": "^7.0.3", - "globals": "^9.2.0", - "ignore": "^3.1.2", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "optionator": "^0.8.1", - "path-is-absolute": "^1.0.0", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.6.0", - "strip-json-comments": "~1.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "shelljs": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", - "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", - "dev": true - }, - "events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", - "dev": true - }, - "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", - "dev": true, - "requires": { - "original": "^1.0.0" - } - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastparse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", - "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", - "dev": true - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", - "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "file-loader": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.2.0.tgz", - "integrity": "sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.0.0" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", - "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", - "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - }, - "dependencies": { - "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "find-index": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", - "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", - "integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==", - "dev": true, - "requires": { - "debug": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "front-matter": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-2.1.2.tgz", - "integrity": "sha1-91mDufL0E75ljJPf172M5AePXNs=", - "dev": true, - "requires": { - "js-yaml": "^3.4.6" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.0.0.tgz", - "integrity": "sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, - "requires": { - "is-property": "^1.0.2" - } - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "glob2base": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", - "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", - "dev": true, - "requires": { - "find-index": "^0.1.1" - } - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - }, - "dependencies": { - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - } - }, - "gonzales-pe-sl": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz", - "integrity": "sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y=", - "dev": true, - "requires": { - "minimist": "1.1.x" - }, - "dependencies": { - "minimist": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", - "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "graphiql": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-0.13.2.tgz", - "integrity": "sha512-4N2HmQQpUfApS1cxrTtoZ15tnR3EW88oUiqmza6GgNQYZZfDdBGphdQlBYsKcjAB/SnIOJort+RA1dB6kf4M7Q==", - "requires": { - "codemirror": "^5.47.0", - "codemirror-graphql": "^0.8.3", - "copy-to-clipboard": "^3.2.0", - "markdown-it": "^8.4.0" - } - }, - "graphql": { - "version": "14.4.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.4.2.tgz", - "integrity": "sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==", - "requires": { - "iterall": "^1.2.2" - } - }, - "graphql-config": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.0.1.tgz", - "integrity": "sha512-eb4FzlODifHE/Q+91QptAmkGw39wL5ToinJ2556UUsGt2drPc4tzifL+HSnHSaxiIbH8EUhc/Fa6+neinF04qA==", - "requires": { - "graphql-import": "^0.4.4", - "graphql-request": "^1.5.0", - "js-yaml": "^3.10.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.4" - } - }, - "graphql-import": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.4.5.tgz", - "integrity": "sha512-G/+I08Qp6/QGTb9qapknCm3yPHV0ZL7wbaalWFpxsfR8ZhZoTBe//LsbsCKlbALQpcMegchpJhpTSKiJjhaVqQ==", - "requires": { - "lodash": "^4.17.4" - } - }, - "graphql-language-service-interface": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-1.3.2.tgz", - "integrity": "sha512-sOxFV5sBSnYtKIFHtlmAHHVdhok7CRbvCPLcuHvL4Q1RSgKRsPpeHUDKU+yCbmlonOKn/RWEKaYWrUY0Sgv70A==", - "requires": { - "graphql-config": "2.0.1", - "graphql-language-service-parser": "^1.2.2", - "graphql-language-service-types": "^1.2.2", - "graphql-language-service-utils": "^1.2.2" - } - }, - "graphql-language-service-parser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.5.0.tgz", - "integrity": "sha512-DX3B6DfvKa28gJoywtnkkIUdZitWqKqBTrZ6CQV8V5wO3GzJalQKT0J+B56oDkS6MhjLt928Yu8fj63laNWfoA==", - "requires": { - "graphql-config": "2.2.1", - "graphql-language-service-types": "^1.5.0" - }, - "dependencies": { - "graphql-config": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", - "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", - "requires": { - "graphql-import": "^0.7.1", - "graphql-request": "^1.5.0", - "js-yaml": "^3.10.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.4" - } - }, - "graphql-import": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", - "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", - "requires": { - "lodash": "^4.17.4", - "resolve-from": "^4.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - } - } - }, - "graphql-language-service-types": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.5.0.tgz", - "integrity": "sha512-THxB15oPC56zlNVSwv7JCahuSUbI9xnUHdftjOqZOz5588qjlPw/UHWQ8V/k0/XwZvH/TwCkmnBkIRmPVb1S5Q==", - "requires": { - "graphql-config": "2.2.1" - }, - "dependencies": { - "graphql-config": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", - "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", - "requires": { - "graphql-import": "^0.7.1", - "graphql-request": "^1.5.0", - "js-yaml": "^3.10.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.4" - } - }, - "graphql-import": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", - "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", - "requires": { - "lodash": "^4.17.4", - "resolve-from": "^4.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - } - } - }, - "graphql-language-service-utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-1.2.2.tgz", - "integrity": "sha512-98hzn1Dg3sSAiB+TuvNwWAoBrzuHs8NylkTK26TFyBjozM5wBZttp+T08OvOt+9hCFYRa43yRPrWcrs78KH9Hw==", - "requires": { - "graphql-config": "2.0.1", - "graphql-language-service-types": "^1.2.2" - } - }, - "graphql-request": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", - "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==", - "requires": { - "cross-fetch": "2.2.2" - } - }, - "handle-thing": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", - "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", - "dev": true - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", - "dev": true - }, - "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", - "dev": true - }, - "html-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", - "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", - "dev": true, - "requires": { - "es6-templates": "^0.2.3", - "fastparse": "^1.1.1", - "html-minifier": "^3.5.8", - "loader-utils": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "html-minifier": { - "version": "3.5.19", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.19.tgz", - "integrity": "sha512-Qr2JC9nsjK8oCrEmuB430ZIA8YWbF3D5LSjywD75FTuXmeqacwHgIM8wp3vHYzzPbklSjp53RdmDuzR4ub2HzA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.1.x", - "commander": "2.16.x", - "he": "1.1.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - }, - "html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", - "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", - "dev": true, - "requires": { - "html-minifier": "^3.2.3", - "loader-utils": "^0.2.16", - "lodash": "^4.17.3", - "pretty-error": "^2.0.2", - "tapable": "^1.0.0", - "toposort": "^1.0.0", - "util.promisify": "1.0.0" - }, - "dependencies": { - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - } - } - }, - "htmlparser2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", - "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.1", - "domutils": "1.1", - "readable-stream": "1.0" - }, - "dependencies": { - "domutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", - "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", - "dev": true - }, - "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", - "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", - "dev": true, - "requires": { - "http-proxy": "^1.17.0", - "is-glob": "^4.0.0", - "lodash": "^4.17.11", - "micromatch": "^3.1.10" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "ignore-loader": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", - "integrity": "sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM=", - "dev": true - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "dependencies": { - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "internal-ip": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", - "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", - "dev": true, - "requires": { - "default-gateway": "^4.2.0", - "ipaddr.js": "^1.9.0" - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", - "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "requires": { - "is-path-inside": "^2.1.0" - } - }, - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-svg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", - "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", - "dev": true, - "requires": { - "html-comment-regex": "^1.1.0" - } - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "requires": { - "buffer-alloc": "^1.2.0" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz", - "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==", - "dev": true, - "requires": { - "async": "^2.6.2", - "compare-versions": "^3.4.0", - "fileset": "^2.0.3", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-hook": "^2.0.7", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", - "js-yaml": "^3.13.1", - "make-dir": "^2.1.0", - "minimatch": "^3.0.4", - "once": "^1.4.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-instrumenter-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", - "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", - "dev": true, - "requires": { - "convert-source-map": "^1.5.0", - "istanbul-lib-instrument": "^1.7.3", - "loader-utils": "^1.1.0", - "schema-utils": "^0.3.0" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "schema-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", - "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", - "dev": true, - "requires": { - "ajv": "^5.0.0" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", - "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": true, - "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" - } - }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", - "dev": true, - "requires": { - "handlebars": "^4.1.2" - } - }, - "iterall": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", - "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" - }, - "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", - "dev": true - }, - "jest-worker": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", - "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json3": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", - "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "karma": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/karma/-/karma-4.3.0.tgz", - "integrity": "sha512-NSPViHOt+RW38oJklvYxQC4BSQsv737oQlr/r06pCM+slDOr4myuI1ivkRmp+3dVpJDfZt2DmaPJ2wkx+ZZuMQ==", - "dev": true, - "requires": { - "bluebird": "^3.3.0", - "body-parser": "^1.16.1", - "braces": "^3.0.2", - "chokidar": "^3.0.0", - "colors": "^1.1.0", - "connect": "^3.6.0", - "core-js": "^3.1.3", - "di": "^0.0.1", - "dom-serialize": "^2.2.0", - "flatted": "^2.0.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "http-proxy": "^1.13.0", - "isbinaryfile": "^3.0.0", - "lodash": "^4.17.14", - "log4js": "^4.0.0", - "mime": "^2.3.1", - "minimatch": "^3.0.2", - "optimist": "^0.6.1", - "qjobs": "^1.1.4", - "range-parser": "^1.2.0", - "rimraf": "^2.6.0", - "safe-buffer": "^5.0.1", - "socket.io": "2.1.1", - "source-map": "^0.6.1", - "tmp": "0.0.33", - "useragent": "2.3.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.1.tgz", - "integrity": "sha512-/j5PPkb5Feyps9e+jo07jUZGvkB5Aj953NrI4s8xSVScrAo/RHeILrtdb4uzR7N6aaFFxxJ+gt8mA8HfNpw76w==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.0", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.1.3" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.0.tgz", - "integrity": "sha512-+iXhW3LuDQsno8dOIrCIT/CBjeBWuP7PXe8w9shnj9Lebny/Gx1ZjVBYwexLz36Ri2jKuXMNpV6CYNh8lHHgrQ==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.3.tgz", - "integrity": "sha512-ZOsfTGkjO2kqeR5Mzr5RYDbTGYneSkdNKX2fOX2P5jF7vMrd/GNnIAUtDldeHHumHUCQ3V05YfWUdxMPAsRu9Q==", - "dev": true, - "requires": { - "picomatch": "^2.0.4" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "karma-chrome-launcher": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", - "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", - "dev": true, - "requires": { - "which": "^1.2.1" - } - }, - "karma-cli": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz", - "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==", - "dev": true, - "requires": { - "resolve": "^1.3.3" - } - }, - "karma-coverage-istanbul-reporter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.0.tgz", - "integrity": "sha512-UH0mXPJFJyK5uiK7EkwGtQ8f30lCBAfqRResnZ4pzLJ04SOp4SPlYkmwbbZ6iVJ6sQFVzlDUXlntBEsLRdgZpg==", - "dev": true, - "requires": { - "istanbul-api": "^2.1.6", - "minimatch": "^3.0.4" - } - }, - "karma-htmlfile-reporter": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/karma-htmlfile-reporter/-/karma-htmlfile-reporter-0.3.8.tgz", - "integrity": "sha512-Hd4c/vqPXYjdNYXeDJRMMq2DMMxPxqOR+TPeiLz2qbqO0qCCQMeXwFGhNDFr+GsvYhcOyn7maTbWusUFchS/4A==", - "dev": true, - "requires": { - "xmlbuilder": "^10.0.0" - } - }, - "karma-jasmine": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", - "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", - "dev": true, - "requires": { - "jasmine-core": "^3.3" - } - }, - "karma-jasmine-html-reporter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.4.2.tgz", - "integrity": "sha512-7g0gPj8+9JepCNJR9WjDyQ2RkZ375jpdurYQyAYv8PorUCadepl8vrD6LmMqOGcM17cnrynBawQYZHaumgDjBw==", - "dev": true - }, - "karma-mocha-reporter": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", - "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=", - "dev": true, - "requires": { - "chalk": "^2.1.0", - "log-symbols": "^2.1.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "karma-sourcemap-loader": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz", - "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "karma-webpack": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz", - "integrity": "sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.1.0", - "neo-async": "^2.6.1", - "schema-utils": "^1.0.0", - "source-map": "^0.7.3", - "webpack-dev-middleware": "^3.7.0" - }, - "dependencies": { - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "known-css-properties": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.3.0.tgz", - "integrity": "sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ==", - "dev": true - }, - "last-call-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", - "dev": true, - "requires": { - "lodash": "^4.17.5", - "webpack-sources": "^1.1.0" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash.capitalize": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", - "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", - "dev": true - }, - "lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "log4js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", - "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", - "dev": true, - "requires": { - "date-format": "^2.0.0", - "debug": "^4.1.1", - "flatted": "^2.0.0", - "rfdc": "^1.1.4", - "streamroller": "^1.0.6" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "loglevel": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.4.tgz", - "integrity": "sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "magic-string": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz", - "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", - "dev": true - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "mersenne-twister": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", - "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "mini-css-extract-plugin": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", - "integrity": "sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "normalize-url": "1.9.1", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "minipass": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.0.1.tgz", - "integrity": "sha512-2y5okJ4uBsjoD2vAbLKL9EUQPPkC0YMIp+2mZOXG3nBba++pdfJWRxx2Ewirc0pwAJYu4XtWg2EkVo1nRXuO/w==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "mousetrap": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz", - "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==" - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "ngx-color-picker": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-8.2.0.tgz", - "integrity": "sha512-rzR+cByjNG9M/UskU5vNoH7cUc6oM8STTDFKOZmnlX4ALOuM1+61CBjsNTGETWfo9a/h5mbGX02oh5/iNAa7vA==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-fetch": { - "version": "2.1.2", - "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", - "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" - }, - "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", - "dev": true - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "dev": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - } - } - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - } - } - }, - "node-releases": { - "version": "1.1.34", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.34.tgz", - "integrity": "sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "node-sass": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", - "dev": true, - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash": "^4.17.11", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "resolve": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", - "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-is": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "oidc-client": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.9.1.tgz", - "integrity": "sha512-AP1BwqASKIYrCBMu9dmNy3OTbhfaiBpy+5hZRbG1dmE2HqpQCp2JiJUNnNGTh2P+cnfVOrC79CGIluD1VMgMzQ==", - "requires": { - "base64-js": "^1.3.0", - "core-js": "^2.6.4", - "crypto-js": "^3.1.9-1", - "uuid": "^3.3.2" - }, - "dependencies": { - "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" - } - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, - "optimize-css-assets-webpack-plugin": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", - "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==", - "dev": true, - "requires": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - } - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-retry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", - "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", - "dev": true, - "requires": { - "retry": "^0.12.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", - "dev": true, - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "optional": true - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pikaday": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.0.tgz", - "integrity": "sha512-SgGxMYX0NHj9oQnMaSyAipr2gOrbB4Lfs/TJTb6H6hRHs39/5c5VZi73Q8hr53+vWjdn6HzkWcj8Vtl3c9ziaA==" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "portfinder": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz", - "integrity": "sha512-ekRl7zD2qxYndYflwiryJwMioBI7LI7rVXg3EnLK3sjkouT5eOuhS3gS255XxBksa30VG8UPZYZCdgfGOfkSUg==", - "dev": true, - "requires": { - "async": "^1.5.2", - "debug": "^2.2.0", - "mkdirp": "0.5.x" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", - "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-calc": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", - "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", - "dev": true, - "requires": { - "css-unit-converter": "^1.1.1", - "postcss": "^7.0.5", - "postcss-selector-parser": "^5.0.0-rc.4", - "postcss-value-parser": "^3.3.1" - }, - "dependencies": { - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "dev": true - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dev": true, - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "requires": { - "postcss": "^7.0.5" - } - }, - "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", - "dev": true, - "requires": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.16", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" - } - }, - "postcss-modules-scope": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz", - "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==", - "dev": true, - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - } - }, - "postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "dev": true, - "requires": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-selector-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", - "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "dependencies": { - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - } - } - }, - "postcss-svgo": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", - "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", - "dev": true, - "requires": { - "is-svg": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - } - }, - "postcss-value-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", - "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", - "dev": true - }, - "postinstall-build": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", - "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", - "dev": true, - "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "progressbar.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/progressbar.js/-/progressbar.js-1.0.1.tgz", - "integrity": "sha1-9/v8GVJA/guzL2972y5/9ADqcfk=", - "requires": { - "shifty": "^1.5.2" - } - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", - "dev": true - }, - "randomatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz", - "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "raw-loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz", - "integrity": "sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^2.0.1" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", - "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "react": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", - "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - } - }, - "react-dom": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz", - "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.16.2" - } - }, - "react-is": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", - "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - } - }, - "recast": { - "version": "0.11.23", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", - "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", - "dev": true, - "requires": { - "ast-types": "0.9.6", - "esprima": "~3.1.0", - "private": "~0.1.5", - "source-map": "~0.5.0" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", - "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2" - } - }, - "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "renderkid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", - "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", - "dev": true, - "requires": { - "css-select": "^1.1.0", - "dom-converter": "~0.1", - "htmlparser2": "~3.3.0", - "strip-ansi": "^3.0.0", - "utila": "~0.3" - }, - "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } - } - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "dependencies": { - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - } - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "rfdc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", - "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", - "dev": true - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, - "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", - "requires": { - "tslib": "^1.9.0" - } - }, - "rxjs-tslint": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/rxjs-tslint/-/rxjs-tslint-0.1.7.tgz", - "integrity": "sha512-NnOfqutNfdT7VQnQm32JLYh2gDZjc0gdWZFtrxf/czNGkLKJ1nOO6jbKAFI09W0f9lCtv6P2ozxjbQH8TSPPFQ==", - "dev": true, - "requires": { - "chalk": "^2.4.0", - "optimist": "^0.6.1", - "tslint": "^5.9.1", - "tsutils": "^2.25.0", - "typescript": ">=2.8.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "sass-lint": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sass-lint/-/sass-lint-1.13.1.tgz", - "integrity": "sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==", - "dev": true, - "requires": { - "commander": "^2.8.1", - "eslint": "^2.7.0", - "front-matter": "2.1.2", - "fs-extra": "^3.0.1", - "glob": "^7.0.0", - "globule": "^1.0.0", - "gonzales-pe-sl": "^4.2.3", - "js-yaml": "^3.5.4", - "known-css-properties": "^0.3.0", - "lodash.capitalize": "^4.1.0", - "lodash.kebabcase": "^4.0.0", - "merge": "^1.2.0", - "path-is-absolute": "^1.0.0", - "util": "^0.10.3" - }, - "dependencies": { - "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "sass-loader": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz", - "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.2.3", - "neo-async": "^2.6.1", - "schema-utils": "^2.1.0", - "semver": "^6.3.0" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.2.0.tgz", - "integrity": "sha512-5EwsCNhfFTZvUreQhx/4vVQpJ/lnCAkgoIHLhSpp4ZirE+4hzFvdJi0FMub6hxbFVBJYSpeVVmon+2e7uEGRrA==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", - "dev": true, - "requires": { - "node-forge": "0.9.0" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "semver-dsl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", - "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", - "dev": true, - "requires": { - "semver": "^5.3.0" - } - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz", - "integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==", - "dev": true - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true, - "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" - } - }, - "shifty": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/shifty/-/shifty-1.5.4.tgz", - "integrity": "sha1-1DYvyRTdKA3fblIr5AiyEgMgg0Y=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "slugify": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.5.tgz", - "integrity": "sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA==" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "socket.io": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", - "dev": true, - "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.1", - "socket.io-parser": "~3.2.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", - "dev": true - }, - "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", - "dev": true, - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", - "to-array": "0.1.4" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", - "dev": true, - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" - } - }, - "sockjs-client": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", - "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", - "dev": true, - "requires": { - "debug": "^3.2.5", - "eventsource": "^1.0.7", - "faye-websocket": "~0.11.1", - "inherits": "^2.0.3", - "json3": "^3.3.2", - "url-parse": "^1.4.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "sourcemap-codec": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", - "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", - "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", - "dev": true - }, - "spdy": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz", - "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.0.1.tgz", - "integrity": "sha512-FfndBvkXL9AHyGLNzU3r9AvYIBBZ7gm+m+kd0p8cT3/v4OliMAyipZAhLVEv1Zi/k4QFq9CstRGVd9pW/zcHFQ==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "minipass": "^3.0.0" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "streamroller": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", - "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", - "dev": true, - "requires": { - "async": "^2.6.2", - "date-format": "^2.0.0", - "debug": "^3.2.6", - "fs-extra": "^7.0.1", - "lodash": "^4.17.14" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "style-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz", - "integrity": "sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.0.1" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", - "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "dev": true, - "requires": { - "dot-prop": "^4.1.1", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "dev": true, - "requires": { - "minimist": "^1.1.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "svgo": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz", - "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.33", - "csso": "^3.5.1", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "dependencies": { - "css-select": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", - "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^2.1.2", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - } - } - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "tapable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", - "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", - "dev": true - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "terser": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.4.tgz", - "integrity": "sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.1.2.tgz", - "integrity": "sha512-MF/C4KABwqYOfRDi87f7gG07GP7Wj/kyiX938UxIGIO6l5mkh8XJL7xtS0hX/CRdVQaZI7ThGUPZbznrCjsGpg==", - "dev": true, - "requires": { - "cacache": "^13.0.0", - "find-cache-dir": "^3.0.0", - "jest-worker": "^24.9.0", - "schema-utils": "^2.4.1", - "serialize-javascript": "^2.1.0", - "source-map": "^0.6.1", - "terser": "^4.3.4", - "webpack-sources": "^1.4.3" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "schema-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz", - "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "thunky": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", - "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==", - "dev": true - }, - "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, - "toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tree-kill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", - "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", - "dev": true - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "dev": true, - "requires": { - "glob": "^7.1.2" - } - }, - "ts-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.0.tgz", - "integrity": "sha512-Da8h3fD+HiZ9GvZJydqzk3mTC9nuOKYlJcpuk+Zv6Y1DPaMvBL+56GRzZFypx2cWrZFMsQr869+Ua2slGoLxvQ==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^1.0.2", - "micromatch": "^4.0.0", - "semver": "^6.0.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "tsconfig-paths": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.5.0.tgz", - "integrity": "sha512-JYbN2zK2mxsv+bDVJCvSTxmdrD4R1qkG908SsqqD8TWjPNbSOtko1mnpQFFJo5Rbbc2/oJgDU9Cpkg/ZD7wNYg==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "deepmerge": "^2.0.1", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "tsconfig-paths-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "tsconfig-paths": "^3.4.0" - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" - }, - "tslint": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.0.tgz", - "integrity": "sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" - }, - "dependencies": { - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } - } - }, - "tslint-immutable": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/tslint-immutable/-/tslint-immutable-5.4.0.tgz", - "integrity": "sha512-8lZG7hNYRFOJv/p/Wb8/1cgizWSRpn4W3GSNWUVye9WyeO/LRbxp88pzNO8Een3RCMbHa3o7oW2UWa+Sx6hCBA==", - "dev": true, - "requires": { - "tsutils": "^2.28.0 || ^3.0.0" - } - }, - "tslint-webpack-plugin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslint-webpack-plugin/-/tslint-webpack-plugin-2.1.0.tgz", - "integrity": "sha512-subYgmwihOGftPZS59looqPWdbqMIvsoTy8MeQPeZ7bOdwZfR3AAnVG8/VzpSRly8l/xbPosrX2QKtJEZPt71A==", - "dev": true, - "requires": { - "chalk": "^2.1.0" - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typemoq": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", - "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "lodash": "^4.17.4", - "postinstall-build": "^5.0.1" - }, - "dependencies": { - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - } - } - }, - "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", - "dev": true - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "uglify-js": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.7.tgz", - "integrity": "sha512-J0M2i1mQA+ze3EdN9SBi751DNdAXmeFLfJrd/MDIkRc3G3Gbb9OPVSx7GIQvVwfWxQARcYV2DTxIkMyDAk3o9Q==", - "dev": true, - "requires": { - "commander": "~2.16.0", - "source-map": "~0.6.1" - } - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true - }, - "uri-js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", - "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "useragent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", - "dev": true, - "requires": { - "lru-cache": "4.1.x", - "tmp": "0.0.x" - } - }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "vendors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", - "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vm-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", - "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", - "dev": true - }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", - "dev": true - }, - "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - } - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "webpack": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz", - "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.1", - "watchpack": "^1.6.0", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", - "dev": true - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", - "dev": true - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - }, - "terser-webpack-plugin": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", - "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "webpack-cli": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz", - "integrity": "sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A==", - "dev": true, - "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - } - } - }, - "webpack-dev-middleware": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz", - "integrity": "sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA==", - "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.2", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - } - } - }, - "webpack-dev-server": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.8.2.tgz", - "integrity": "sha512-0xxogS7n5jHDQWy0WST0q6Ykp7UGj4YvWh+HVN71JoE7BwPxMZrwgraBvmdEMbDVMBzF0u+mEzn8TQzBm5NYJQ==", - "dev": true, - "requires": { - "ansi-html": "0.0.7", - "bonjour": "^3.5.0", - "chokidar": "^2.1.8", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "debug": "^4.1.1", - "del": "^4.1.1", - "express": "^4.17.1", - "html-entities": "^1.2.1", - "http-proxy-middleware": "0.19.1", - "import-local": "^2.0.0", - "internal-ip": "^4.3.0", - "ip": "^1.1.5", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "loglevel": "^1.6.4", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.24", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.7", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "0.3.19", - "sockjs-client": "1.4.0", - "spdy": "^4.0.1", - "strip-ansi": "^3.0.1", - "supports-color": "^6.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^3.7.2", - "webpack-log": "^2.0.0", - "ws": "^6.2.1", - "yargs": "12.0.5" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "webpack-dev-middleware": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", - "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", - "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.4", - "mkdirp": "^0.5.1", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - } - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - }, - "webpack-sources": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", - "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", - "dev": true - }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "xhr2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", - "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" - }, - "xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "dev": true - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.1.0.tgz", - "integrity": "sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - } - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - } - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", - "dev": true - }, - "zone.js": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.2.tgz", - "integrity": "sha512-UAYfiuvxLN4oyuqhJwd21Uxb4CNawrq6fPS/05Su5L4G+1TN+HVDJMUHNMobVQDFJRir2cLAODXwluaOKB7HFg==" - } - } -} diff --git a/src/Squidex/package.json b/src/Squidex/package.json deleted file mode 100644 index 13b16acf7..000000000 --- a/src/Squidex/package.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "name": "squidex", - "version": "1.0.0", - "description": "Squidex Headless CMS", - "license": "MIT", - "repository": "https://github.com/SebastianStehle/Squidex", - "scripts": { - "copy": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/", - "start": "npm run copy && webpack-dev-server --config app-config/webpack.config.js --inline --port 3000 --hot", - "test": "karma start", - "test:coverage": "karma start karma.coverage.conf.js", - "test:clean": "rimraf _test-output", - "tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts", - "build": "npm run copy && node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js --config app-config/webpack.config.js --env.production", - "build:clean": "rimraf wwwroot/build" - }, - "dependencies": { - "@angular/animations": "8.2.9", - "@angular/cdk": "8.2.3", - "@angular/common": "8.2.9", - "@angular/core": "8.2.9", - "@angular/forms": "8.2.9", - "@angular/http": "7.2.15", - "@angular/platform-browser": "8.2.9", - "@angular/platform-browser-dynamic": "8.2.9", - "@angular/platform-server": "8.2.9", - "@angular/router": "8.2.9", - "angular2-chartjs": "0.5.1", - "babel-polyfill": "6.26.0", - "bootstrap": "4.3.1", - "core-js": "3.2.1", - "graphiql": "0.13.2", - "graphql": "14.4.2", - "marked": "0.7.0", - "mersenne-twister": "1.1.0", - "moment": "2.24.0", - "mousetrap": "1.6.3", - "ngx-color-picker": "8.2.0", - "oidc-client": "1.9.1", - "pikaday": "1.8.0", - "progressbar.js": "1.0.1", - "react": "16.10.2", - "react-dom": "16.10.2", - "rxjs": "6.5.3", - "slugify": "1.3.5", - "tslib": "1.10.0", - "zone.js": "0.10.2" - }, - "devDependencies": { - "@angular-devkit/build-optimizer": "0.803.8", - "@angular/compiler": "8.2.9", - "@angular/compiler-cli": "8.2.9", - "@ngtools/webpack": "8.3.8", - "@types/core-js": "2.5.2", - "@types/jasmine": "3.4.2", - "@types/marked": "0.6.5", - "@types/mersenne-twister": "1.1.2", - "@types/mousetrap": "1.6", - "@types/node": "12.7.11", - "@types/react": "16.9.5", - "@types/react-dom": "16.9.1", - "@types/sortablejs": "1.7.2", - "browserslist": "4.7.0", - "caniuse-lite": "1.0.30000998", - "circular-dependency-plugin": "5.2.0", - "codelyzer": "5.1.2", - "cpx": "1.5.0", - "css-loader": "3.2.0", - "file-loader": "4.2.0", - "html-loader": "0.5.5", - "html-webpack-plugin": "3.2.0", - "ignore-loader": "0.1.2", - "istanbul-instrumenter-loader": "3.0.1", - "jasmine-core": "3.5.0", - "karma": "4.3.0", - "karma-chrome-launcher": "3.1.0", - "karma-cli": "2.0.0", - "karma-coverage-istanbul-reporter": "2.1.0", - "karma-htmlfile-reporter": "0.3.8", - "karma-jasmine": "2.0.1", - "karma-jasmine-html-reporter": "1.4.2", - "karma-mocha-reporter": "2.2.5", - "karma-sourcemap-loader": "0.3.7", - "karma-webpack": "4.0.2", - "mini-css-extract-plugin": "0.8.0", - "node-sass": "4.12.0", - "optimize-css-assets-webpack-plugin": "5.0.3", - "raw-loader": "3.1.0", - "rimraf": "3.0.0", - "rxjs-tslint": "0.1.7", - "sass-lint": "1.13.1", - "sass-loader": "8.0.0", - "style-loader": "1.0.0", - "terser-webpack-plugin": "2.1.2", - "ts-loader": "6.2.0", - "tsconfig-paths-webpack-plugin": "3.2.0", - "tslint": "5.20.0", - "tslint-immutable": "5.4.0", - "tslint-webpack-plugin": "2.1.0", - "typemoq": "2.1.0", - "typescript": "3.5.3", - "underscore": "1.9.1", - "webpack": "4.41.0", - "webpack-cli": "3.3.9", - "webpack-dev-server": "3.8.2" - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs deleted file mode 100644 index b96f6637d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Apps -{ - public class RoleTests - { - [Fact] - public void Should_be_default_role() - { - var role = new Role("Owner"); - - Assert.True(role.IsDefault); - } - - [Fact] - public void Should_not_be_default_role() - { - var role = new Role("Custom"); - - Assert.False(role.IsDefault); - } - - [Fact] - public void Should_add_common_permission() - { - var role = new Role("Name"); - - var result = role.ForApp("my-app").Permissions.ToIds(); - - Assert.Equal(new[] { "squidex.apps.my-app.common" }, result); - } - - [Fact] - public void Should_not_have_duplicate_permission() - { - var role = new Role("Name", "common", "common", "common"); - - var result = role.ForApp("my-app").Permissions.ToIds(); - - Assert.Single(result); - } - - [Fact] - public void Should_ForApp_permission() - { - var role = new Role("Name", "clients.read"); - - var result = role.ForApp("my-app").Permissions.ToIds(); - - Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(1)); - } - - [Fact] - public void Should_check_for_name() - { - var role = new Role("Custom"); - - Assert.True(role.Equals("Custom")); - } - - [Fact] - public void Should_check_for_null_name() - { - var role = new Role("Custom"); - - Assert.False(role.Equals(null)); - Assert.False(role.Equals("Other")); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs deleted file mode 100644 index 2a38e6a10..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure.Security; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Core.Model.Apps -{ - public class RolesTests - { - private readonly Roles roles_0; - private readonly string firstRole = "Role1"; - private readonly string role = "Role2"; - - public RolesTests() - { - roles_0 = Roles.Empty.Add(firstRole); - } - - [Fact] - public void Should_create_roles_without_defaults() - { - var roles = new Roles(Roles.Defaults.ToArray()); - - Assert.Equal(0, roles.CustomCount); - } - - [Fact] - public void Should_add_role() - { - var roles_1 = roles_0.Add(role); - - roles_1[role].Should().BeEquivalentTo(new Role(role, PermissionSet.Empty)); - } - - [Fact] - public void Should_throw_exception_if_add_role_with_same_name() - { - var roles_1 = roles_0.Add(role); - - Assert.Throws(() => roles_1.Add(role)); - } - - [Fact] - public void Should_do_nothing_if_role_to_add_is_default() - { - var roles_1 = roles_0.Add(Role.Developer); - - Assert.True(roles_1.CustomCount > 0); - } - - [Fact] - public void Should_update_role() - { - var roles_1 = roles_0.Update(firstRole, "P1", "P2"); - - roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2"))); - } - - [Fact] - public void Should_return_same_roles_if_role_not_found() - { - var roles_1 = roles_0.Update(role, "P1", "P2"); - - Assert.Same(roles_0, roles_1); - } - - [Fact] - public void Should_remove_role() - { - var roles_1 = roles_0.Remove(firstRole); - - Assert.Equal(0, roles_1.CustomCount); - } - - [Fact] - public void Should_do_nothing_if_remove_role_not_found() - { - var roles_1 = roles_0.Remove(role); - - Assert.True(roles_1.CustomCount > 0); - } - - [Fact] - public void Should_get_custom_roles() - { - var names = roles_0.Custom.Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { firstRole }, names); - } - - [Fact] - public void Should_get_all_roles() - { - var names = roles_0.All.Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); - } - - [Fact] - public void Should_check_for_custom_role() - { - Assert.True(roles_0.ContainsCustom(firstRole)); - } - - [Fact] - public void Should_check_for_non_custom_role() - { - Assert.False(roles_0.ContainsCustom(Role.Owner)); - } - - [Fact] - public void Should_check_for_default_role() - { - Assert.True(Roles.IsDefault(Role.Owner)); - } - - [Fact] - public void Should_check_for_non_default_role() - { - Assert.False(Roles.IsDefault(firstRole)); - } - - [InlineData("Developer")] - [InlineData("Editor")] - [InlineData("Owner")] - [InlineData("Reader")] - [Theory] - public void Should_get_default_roles(string name) - { - var found = roles_0.TryGet("app", name, out var role); - - Assert.True(found); - Assert.True(role.IsDefault); - Assert.True(roles_0.Contains(name)); - - foreach (var permission in role.Permissions) - { - Assert.StartsWith("squidex.apps.app.", permission.Id); - } - } - - [Fact] - public void Should_return_null_if_role_not_found() - { - var found = roles_0.TryGet("app", "custom", out var role); - - Assert.False(found); - Assert.Null(role); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs deleted file mode 100644 index e0c7307db..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Contents.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Contents -{ - public class WorkflowJsonTests - { - [Fact] - public void Should_serialize_and_deserialize() - { - var workflow = Workflow.Default; - - var serialized = workflow.SerializeAndDeserialize(); - - serialized.Should().BeEquivalentTo(workflow); - } - - [Fact] - public void Should_verify_roles_mapping_in_workflow_transition() - { - var source = new JsonWorkflowTransition { Expression = "expression_1", Role = "role_1" }; - - var serialized = source.SerializeAndDeserialize(); - - var result = serialized.ToTransition(); - - Assert.Single(result.Roles); - Assert.Equal(source.Role, result.Roles[0]); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs deleted file mode 100644 index f0601b933..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Collections; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Contents -{ - public class WorkflowTests - { - private readonly Workflow workflow = new Workflow( - Status.Draft, new Dictionary - { - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition("ToArchivedExpr", ReadOnlyCollection.Create("ToArchivedRole" )), - [Status.Published] = new WorkflowTransition("ToPublishedExpr", ReadOnlyCollection.Create("ToPublishedRole" )) - }, - StatusColors.Draft), - [Status.Archived] = - new WorkflowStep(), - [Status.Published] = - new WorkflowStep() - }); - - [Fact] - public void Should_provide_default_workflow_if_none_found() - { - var result = Workflows.Empty.GetFirst(); - - Assert.Same(Workflow.Default, result); - } - - [Fact] - public void Should_provide_initial_state() - { - var (status, step) = workflow.GetInitialStep(); - - Assert.Equal(Status.Draft, status); - Assert.Equal(StatusColors.Draft, step.Color); - Assert.Same(workflow.Steps[Status.Draft], step); - } - - [Fact] - public void Should_provide_step() - { - var found = workflow.TryGetStep(Status.Draft, out var step); - - Assert.True(found); - Assert.Same(workflow.Steps[Status.Draft], step); - } - - [Fact] - public void Should_not_provide_unknown_step() - { - var found = workflow.TryGetStep(default, out var step); - - Assert.False(found); - Assert.Null(step); - } - - [Fact] - public void Should_provide_transition() - { - var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition); - - Assert.True(found); - Assert.Equal("ToArchivedExpr", transition.Expression); - Assert.Equal(new[] { "ToArchivedRole" }, transition.Roles); - } - - [Fact] - public void Should_provide_transition_to_initial_if_step_not_found() - { - var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition); - - Assert.True(found); - Assert.Null(transition.Expression); - Assert.Null(transition.Roles); - } - - [Fact] - public void Should_not_provide_transition_from_unknown_step() - { - var found = workflow.TryGetTransition(new Status("Other"), Status.Archived, out var transition); - - Assert.False(found); - Assert.Null(transition); - } - - [Fact] - public void Should_not_provide_transition_to_unknown_step() - { - var found = workflow.TryGetTransition(Status.Draft, default, out var transition); - - Assert.False(found); - Assert.Null(transition); - } - - [Fact] - public void Should_provide_transitions() - { - var transitions = workflow.GetTransitions(Status.Draft).ToArray(); - - Assert.Equal(2, transitions.Length); - - var (status1, step1, transition1) = transitions[0]; - - Assert.Equal(Status.Archived, status1); - Assert.Equal("ToArchivedExpr", transition1.Expression); - - Assert.Equal(new[] { "ToArchivedRole" }, transition1.Roles); - Assert.Same(workflow.Steps[status1], step1); - - var (status2, step2, transition2) = transitions[1]; - - Assert.Equal(Status.Published, status2); - Assert.Equal("ToPublishedExpr", transition2.Expression); - Assert.Equal(new[] { "ToPublishedRole" }, transition2.Roles); - Assert.Same(workflow.Steps[status2], step2); - } - - [Fact] - public void Should_provide_transitions_to_initial_step_if_status_not_found() - { - var transitions = workflow.GetTransitions(new Status("Other")).ToArray(); - - Assert.Single(transitions); - - var (status1, step1, transition1) = transitions[0]; - - Assert.Equal(Status.Draft, status1); - Assert.Null(transition1.Expression); - Assert.Null(transition1.Roles); - Assert.Same(workflow.Steps[status1], step1); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs deleted file mode 100644 index f16cdf930..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model -{ - public class PartitioningTests - { - [Fact] - public void Should_consider_null_as_valid_partitioning() - { - string partitioning = null; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_consider_invariant_as_valid_partitioning() - { - var partitioning = "invariant"; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_consider_language_as_valid_partitioning() - { - var partitioning = "language"; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_not_consider_empty_as_valid_partitioning() - { - var partitioning = string.Empty; - - Assert.False(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_not_consider_other_string_as_valid_partitioning() - { - var partitioning = "invalid"; - - Assert.False(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_provide_invariant_instance() - { - Assert.Equal("invariant", Partitioning.Invariant.Key); - Assert.Equal("invariant", Partitioning.Invariant.ToString()); - } - - [Fact] - public void Should_provide_language_instance() - { - Assert.Equal("language", Partitioning.Language.Key); - Assert.Equal("language", Partitioning.Language.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var partitioning1_a = new Partitioning("partitioning1"); - var partitioning1_b = new Partitioning("partitioning1"); - - var partitioning2 = new Partitioning("partitioning2"); - - Assert.Equal(partitioning1_a, partitioning1_b); - Assert.Equal(partitioning1_a.GetHashCode(), partitioning1_b.GetHashCode()); - Assert.True(partitioning1_a.Equals((object)partitioning1_b)); - - Assert.NotEqual(partitioning1_a, partitioning2); - Assert.NotEqual(partitioning1_a.GetHashCode(), partitioning2.GetHashCode()); - Assert.False(partitioning1_a.Equals((object)partitioning2)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs deleted file mode 100644 index eeaf343b7..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// 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 FluentAssertions; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Reflection; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Core.Model.Rules -{ - public class RuleTests - { - public static readonly List Triggers = - typeof(Rule).Assembly.GetTypes() - .Where(x => x.BaseType == typeof(RuleTrigger)) - .Select(Activator.CreateInstance) - .Select(x => new[] { x }) - .ToList(); - - private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1()); - - public sealed class OtherTrigger : RuleTrigger - { - public override T Accept(IRuleTriggerVisitor visitor) - { - throw new NotSupportedException(); - } - } - - public sealed class MigratedTrigger : RuleTrigger, IMigrated - { - public override T Accept(IRuleTriggerVisitor visitor) - { - throw new NotSupportedException(); - } - - public RuleTrigger Migrate() - { - return new OtherTrigger(); - } - } - - [TypeName(nameof(TestAction1))] - public sealed class TestAction1 : RuleAction - { - public string Property { get; set; } - } - - [TypeName(nameof(TestAction2))] - public sealed class TestAction2 : RuleAction - { - public string Property { get; set; } - } - - [Fact] - public void Should_create_with_trigger_and_action() - { - var ruleTrigger = new ContentChangedTriggerV2(); - var ruleAction = new TestAction1(); - - var newRule = new Rule(ruleTrigger, ruleAction); - - Assert.Equal(ruleTrigger, newRule.Trigger); - Assert.Equal(ruleAction, newRule.Action); - Assert.True(newRule.IsEnabled); - } - - [Fact] - public void Should_set_enabled_to_true_when_enabling() - { - var rule_1 = rule_0.Disable(); - var rule_2 = rule_1.Enable(); - var rule_3 = rule_2.Enable(); - - Assert.False(rule_1.IsEnabled); - Assert.True(rule_3.IsEnabled); - } - - [Fact] - public void Should_set_enabled_to_false_when_disabling() - { - var rule_1 = rule_0.Disable(); - var rule_2 = rule_1.Disable(); - - Assert.True(rule_0.IsEnabled); - Assert.False(rule_2.IsEnabled); - } - - [Fact] - public void Should_replace_name_when_renaming() - { - var rule_1 = rule_0.Rename("MyName"); - - Assert.Equal("MyName", rule_1.Name); - } - - [Fact] - public void Should_replace_trigger_when_updating() - { - var newTrigger = new ContentChangedTriggerV2(); - - var rule_1 = rule_0.Update(newTrigger); - - Assert.NotSame(newTrigger, rule_0.Trigger); - Assert.Same(newTrigger, rule_1.Trigger); - } - - [Fact] - public void Should_throw_exception_when_new_trigger_has_other_type() - { - Assert.Throws(() => rule_0.Update(new OtherTrigger())); - } - - [Fact] - public void Should_replace_action_when_updating() - { - var newAction = new TestAction1(); - - var rule_1 = rule_0.Update(newAction); - - Assert.NotSame(newAction, rule_0.Action); - Assert.Same(newAction, rule_1.Action); - } - - [Fact] - public void Should_throw_exception_when_new_action_has_other_type() - { - Assert.Throws(() => rule_0.Update(new TestAction2())); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var rule_1 = rule_0.Disable(); - - var serialized = rule_1.SerializeAndDeserialize(); - - serialized.Should().BeEquivalentTo(rule_1); - } - - [Fact] - public void Should_serialize_and_deserialize_and_migrate_trigger() - { - var rule_X = new Rule(new MigratedTrigger(), new TestAction1()); - - var serialized = rule_X.SerializeAndDeserialize(); - - Assert.IsType(serialized.Trigger); - } - - [Theory] - [MemberData(nameof(Triggers))] - public void Should_freeze_triggers(RuleTrigger trigger) - { - TestUtils.TestFreeze(trigger); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs deleted file mode 100644 index 439600188..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ========================================================================== -// 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.Domain.Apps.Core.Schemas; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Core.Model.Schemas -{ - public class SchemaFieldTests - { - public static readonly List FieldProperties = - typeof(Schema).Assembly.GetTypes() - .Where(x => x.BaseType == typeof(FieldProperties)) - .Select(Activator.CreateInstance) - .Select(x => new[] { x }) - .ToList(); - - private readonly RootField field_0 = Fields.Number(1, "my-field", Partitioning.Invariant); - - [Fact] - public void Should_instantiate_field() - { - Assert.True(field_0.RawProperties.IsFrozen); - Assert.Equal("my-field", field_0.Name); - } - - [Fact] - public void Should_throw_exception_if_creating_field_with_invalid_name() - { - Assert.Throws(() => Fields.Number(1, string.Empty, Partitioning.Invariant)); - } - - [Fact] - public void Should_hide_field() - { - var field_1 = field_0.Hide(); - var field_2 = field_1.Hide(); - - Assert.False(field_0.IsHidden); - Assert.True(field_2.IsHidden); - } - - [Fact] - public void Should_show_field() - { - var field_1 = field_0.Hide(); - var field_2 = field_1.Show(); - var field_3 = field_2.Show(); - - Assert.True(field_1.IsHidden); - Assert.False(field_3.IsHidden); - } - - [Fact] - public void Should_disable_field() - { - var field_1 = field_0.Disable(); - var field_2 = field_1.Disable(); - - Assert.False(field_0.IsDisabled); - Assert.True(field_2.IsDisabled); - } - - [Fact] - public void Should_enable_field() - { - var field_1 = field_0.Disable(); - var field_2 = field_1.Enable(); - var field_3 = field_2.Enable(); - - Assert.True(field_1.IsDisabled); - Assert.False(field_3.IsDisabled); - } - - [Fact] - public void Should_lock_field() - { - var field_1 = field_0.Lock(); - - Assert.False(field_0.IsLocked); - Assert.True(field_1.IsLocked); - } - - [Fact] - public void Should_update_field() - { - var field_1 = field_0.Update(new NumberFieldProperties { Hints = "my-hints" }); - - Assert.Null(field_0.RawProperties.Hints); - Assert.True(field_1.RawProperties.IsFrozen); - Assert.Equal("my-hints", field_1.RawProperties.Hints); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() - { - Assert.Throws(() => field_0.Update(new StringFieldProperties())); - } - - [Theory] - [MemberData(nameof(FieldProperties))] - public void Should_freeze_field_properties(FieldProperties action) - { - TestUtils.TestFreeze(action); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs deleted file mode 100644 index 0429afaab..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. - -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent -{ - public class ContentConversionFlatTests - { - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - - [Fact] - public void Should_return_original_when_no_language_preferences_defined() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)); - - Assert.Same(data, data.ToFlatLanguageModel(languagesConfig)); - } - - [Fact] - public void Should_return_flatten_value() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var output = data.ToFlatten(); - - var expected = new Dictionary - { - { - "field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2) - }, - { - "field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4) - }, - { "field3", JsonValue.Create(6) }, - { "field4", JsonValue.Create(7) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - - [Fact] - public void Should_return_flat_list_when_single_languages_specified() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var fallbackConfig = - LanguagesConfig.Build( - new LanguageConfig(Language.EN), - new LanguageConfig(Language.DE, false, Language.EN)); - - var output = (Dictionary)data.ToFlatLanguageModel(fallbackConfig, new List { Language.DE }); - - var expected = new Dictionary - { - { "field1", JsonValue.Create(1) }, - { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - - [Fact] - public void Should_return_flat_list_when_languages_specified() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var output = (Dictionary)data.ToFlatLanguageModel(languagesConfig, new List { Language.DE, Language.EN }); - - var expected = new Dictionary - { - { "field1", JsonValue.Create(1) }, - { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs deleted file mode 100644 index f525b284c..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.EnrichContent; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions - -namespace Squidex.Domain.Apps.Core.Operations.EnrichContent -{ - public class ContentEnrichmentTests - { - private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); - private readonly Schema schema; - - public ContentEnrichmentTests() - { - schema = - new Schema("my-schema") - .AddString(1, "my-string", Partitioning.Language, - new StringFieldProperties { DefaultValue = "en-string" }) - .AddNumber(2, "my-number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddDateTime(3, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = now }) - .AddBoolean(4, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true }); - } - - [Fact] - private void Should_enrich_with_default_values() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "de-string")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal(456, ((JsonScalar)data["my-number"]["iv"]).Value); - - Assert.Equal("de-string", data["my-string"]["de"].ToString()); - Assert.Equal("en-string", data["my-string"]["en"].ToString()); - - Assert.Equal(now.ToString(), data["my-datetime"]["iv"].ToString()); - - Assert.True(((JsonScalar)data["my-boolean"]["iv"]).Value); - } - - [Fact] - private void Should_also_enrich_with_default_values_when_string_is_empty() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", string.Empty)) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal("en-string", data["my-string"]["de"].ToString()); - Assert.Equal("en-string", data["my-string"]["en"].ToString()); - } - - [Fact] - public void Should_get_default_value_from_assets_field() - { - var field = - Fields.Assets(1, "1", Partitioning.Invariant, - new AssetsFieldProperties()); - - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_boolean_field() - { - var field = - Fields.Boolean(1, "1", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true }); - - Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); - - Assert.Equal(JsonValue.Create(FutureDays(15).ToString()), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_when_set_to_today() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); - - Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_when_set_to_now() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); - - Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_json_field() - { - var field = - Fields.Json(1, "1", Partitioning.Invariant, - new JsonFieldProperties()); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_geolocation_field() - { - var field = - Fields.Geolocation(1, "1", Partitioning.Invariant, - new GeolocationFieldProperties()); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_number_field() - { - var field = - Fields.Number(1, "1", Partitioning.Invariant, - new NumberFieldProperties { DefaultValue = 12 }); - - Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_references_field() - { - var field = - Fields.References(1, "1", Partitioning.Invariant, - new ReferencesFieldProperties()); - - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_string_field() - { - var field = - Fields.String(1, "1", Partitioning.Invariant, - new StringFieldProperties { DefaultValue = "default" }); - - Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - [Fact] - public void Should_get_default_value_from_tags_field() - { - var field = - Fields.Tags(1, "1", Partitioning.Invariant, - new TagsFieldProperties()); - - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); - } - - private Instant FutureDays(int days) - { - return now.WithoutMs().Plus(Duration.FromDays(days)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs deleted file mode 100644 index 56d0093df..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ /dev/null @@ -1,607 +0,0 @@ - -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.EventSynchronization; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization -{ - public class SchemaSynchronizerTests - { - private readonly Func idGenerator; - private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer; - private readonly NamedId stringId = NamedId.Of(13L, "my-value"); - private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); - private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); - private int fields = 50; - - public SchemaSynchronizerTests() - { - idGenerator = () => fields++; - } - - [Fact] - public void Should_create_events_if_schema_deleted() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - (Schema)null; - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaDeleted() - ); - } - - [Fact] - public void Should_create_events_if_category_changed() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .ChangeCategory("Category"); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaCategoryChanged { Name = "Category" } - ); - } - - [Fact] - public void Should_create_events_if_scripts_configured() - { - var scripts = new SchemaScripts - { - Create = "" - }; - - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target").ConfigureScripts(scripts); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaScriptsConfigured { Scripts = scripts } - ); - } - - [Fact] - public void Should_create_events_if_preview_urls_configured() - { - var previewUrls = new Dictionary - { - ["web"] = "Url" - }; - - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .ConfigurePreviewUrls(previewUrls); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } - ); - } - - [Fact] - public void Should_create_events_if_schema_published() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .Publish(); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaPublished() - ); - } - - [Fact] - public void Should_create_events_if_schema_unpublished() - { - var sourceSchema = - new Schema("source") - .Publish(); - - var targetSchema = - new Schema("target"); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaUnpublished() - ); - } - - [Fact] - public void Should_create_events_if_nested_field_deleted() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_deleted() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target"); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_updated() - { - var properties = new StringFieldProperties { IsRequired = true }; - - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name, properties)); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_updated() - { - var properties = new StringFieldProperties { IsRequired = true }; - - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldUpdated { Properties = properties, FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_locked() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .LockField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_locked() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .LockField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldLocked { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_hidden() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_hidden() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldHidden { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_shown() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_shown() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldShown { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_disabled() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .DisableField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_disabled() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .DisableField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDisabled { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_enabled() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .DisableField(nestedId.Id, arrayId.Id); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_enabled() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .DisableField(stringId.Id); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldEnabled { FieldId = stringId } - ); - } - - [Fact] - public void Should_create_events_if_field_created() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var createdId = NamedId.Of(50L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new FieldHidden { FieldId = createdId } - ); - } - - [Fact] - public void Should_create_events_if_field_type_has_changed() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var createdId = NamedId.Of(50L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId }, - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() } - ); - } - - [Fact] - public void Should_create_events_if_field_partitioning_has_changed() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Language); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var createdId = NamedId.Of(50L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId }, - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Language.Key, Properties = new StringFieldProperties() } - ); - } - - [Fact] - public void Should_create_events_if_nested_field_created() - { - var sourceSchema = - new Schema("source"); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - var id1 = NamedId.Of(50L, arrayId.Name); - var id2 = NamedId.Of(51L, stringId.Name); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, - new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, - new FieldHidden { FieldId = id2, ParentFieldId = id1 } - ); - } - - [Fact] - public void Should_create_events_if_nested_fields_reordered() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(10, "f1") - .AddString(11, "f2")); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(1, "f2") - .AddString(2, "f1")); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new List { 11, 10 }, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f2", Partitioning.Invariant) - .AddString(2, "f1", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered_after_sync() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f3", Partitioning.Invariant) - .AddString(2, "f1", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 50, 10 } } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered_after_sync2() - { - var id1 = NamedId.Of(1, "f1"); - var id2 = NamedId.Of(2, "f1"); - - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f1", Partitioning.Invariant) - .AddString(2, "f3", Partitioning.Invariant) - .AddString(3, "f2", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 10, 50, 11 } } - ); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs deleted file mode 100644 index 7573a5194..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ /dev/null @@ -1,308 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.ExtractReferenceIds; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. - -namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds -{ - public class ReferenceExtractionTests - { - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Schema schema; - - public ReferenceExtractionTests() - { - schema = - new Schema("my-schema") - .AddNumber(1, "field1", Partitioning.Language) - .AddNumber(2, "field2", Partitioning.Invariant) - .AddNumber(3, "field3", Partitioning.Invariant) - .AddAssets(5, "assets1", Partitioning.Invariant) - .AddAssets(6, "assets2", Partitioning.Invariant) - .AddArray(7, "array", Partitioning.Invariant, a => a - .AddAssets(71, "assets71")) - .AddJson(4, "json", Partitioning.Language) - .UpdateField(3, f => f.Hide()); - } - - [Fact] - public void Should_get_ids_from_id_data() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new IdContentData() - .AddField(5, - new ContentFieldData() - .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); - - var ids = input.GetReferencedIds(schema).ToArray(); - - Assert.Equal(new[] { id1, id2 }, ids); - } - - [Fact] - public void Should_get_ids_from_name_data() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new NamedContentData() - .AddField("assets1", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); - - var ids = input.GetReferencedIds(schema).ToArray(); - - Assert.Equal(new[] { id1, id2 }, ids); - } - - [Fact] - public void Should_cleanup_deleted_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new IdContentData() - .AddField(5, - new ContentFieldData() - .AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); - - var converter = FieldConverters.ForValues(ValueReferencesConverter.CleanReferences(new[] { id2 })); - - var actual = input.ConvertId2Id(schema, converter); - - var cleanedValue = (JsonArray)actual[5]["iv"]; - - Assert.Equal(1, cleanedValue.Count); - Assert.Equal(id1.ToString(), cleanedValue[0].ToString()); - } - - [Fact] - public void Should_return_ids_from_assets_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2 }, result); - } - - [Fact] - public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_null() - { - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(null).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_other_type() - { - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_empty_list_from_non_references_field() - { - var sut = Fields.String(1, "my-string", Partitioning.Invariant); - - var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_null_from_assets_field_when_removing_references_from_null_array() - { - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.CleanReferences(JsonValue.Null, null); - - Assert.Equal(JsonValue.Null, result); - } - - [Fact] - public void Should_remove_deleted_references_from_assets_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); - - var token = CreateValue(id1, id2); - - var result = sut.CleanReferences(token, HashSet.Of(Guid.NewGuid())); - - Assert.Same(token, result); - } - - [Fact] - public void Should_return_ids_from_nested_references_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = - Fields.Array(1, "my-array", Partitioning.Invariant, - Fields.References(1, "my-refs", - new ReferencesFieldProperties { SchemaId = schemaId })); - - var value = - JsonValue.Array( - JsonValue.Object() - .Add("my-refs", CreateValue(id1, id2))); - - var result = sut.GetReferencedIds(value).ToArray(); - - Assert.Equal(new[] { id1, id2, schemaId }, result); - } - - [Fact] - public void Should_return_ids_from_references_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2, schemaId }, result); - } - - [Fact] - public void Should_return_ids_from_references_field_without_schema_id() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(CreateValue(id1, id2), Ids.ContentOnly).ToArray(); - - Assert.Equal(new[] { id1, id2 }, result); - } - - [Fact] - public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() - { - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(JsonValue.Null).ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() - { - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_null_from_references_field_when_removing_references_from_null_array() - { - var sut = Fields.References(1, "my-refs", Partitioning.Invariant); - - var result = sut.CleanReferences(JsonValue.Null, null); - - Assert.Equal(JsonValue.Null, result); - } - - [Fact] - public void Should_remove_deleted_references_from_references_field() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_remove_all_references_from_references_field_when_schema_is_removed() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(schemaId)); - - Assert.Equal(CreateValue(), result); - } - - [Fact] - public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = Fields.References(1, "my-refs", Partitioning.Invariant); - - var value = CreateValue(id1, id2); - - var result = sut.CleanReferences(value, HashSet.Of(Guid.NewGuid())); - - Assert.Same(value, result); - } - - private static IJsonValue CreateValue(params Guid[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs deleted file mode 100644 index 636c85caa..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ /dev/null @@ -1,331 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Options; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Reflection; -using Xunit; - -#pragma warning disable xUnit2009 // Do not use boolean check to check for string equality - -namespace Squidex.Domain.Apps.Core.Operations.HandleRules -{ - public class RuleServiceTests - { - private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake(); - private readonly IRuleActionHandler ruleActionHandler = A.Fake(); - private readonly IEventEnricher eventEnricher = A.Fake(); - private readonly IClock clock = A.Fake(); - private readonly string actionData = "{\"value\":10}"; - private readonly string actionDump = "MyDump"; - private readonly string actionName = "ValidAction"; - private readonly string actionDescription = "MyDescription"; - private readonly Guid ruleId = Guid.NewGuid(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - private readonly RuleService sut; - - public sealed class InvalidEvent : IEvent - { - } - - public sealed class InvalidAction : RuleAction - { - } - - public sealed class ValidAction : RuleAction - { - } - - public sealed class ValidData - { - public int Value { get; set; } - } - - public sealed class InvalidTrigger : RuleTrigger - { - public override T Accept(IRuleTriggerVisitor visitor) - { - return default; - } - } - - public RuleServiceTests() - { - typeNameRegistry.Map(typeof(ContentCreated)); - typeNameRegistry.Map(typeof(ValidAction), actionName); - - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - - A.CallTo(() => ruleActionHandler.ActionType) - .Returns(typeof(ValidAction)); - - A.CallTo(() => ruleActionHandler.DataType) - .Returns(typeof(ValidData)); - - A.CallTo(() => ruleTriggerHandler.TriggerType) - .Returns(typeof(ContentChangedTriggerV2)); - - var log = A.Fake(); - - sut = new RuleService(Options.Create(new RuleOptions()), - new[] { ruleTriggerHandler }, - new[] { ruleActionHandler }, - eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry); - } - - [Fact] - public async Task Should_not_create_job_if_rule_disabled() - { - var @event = Envelope.Create(new ContentCreated()); - - var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_for_invalid_event() - { - var @event = Envelope.Create(new InvalidEvent()); - - var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_no_trigger_handler_registered() - { - var @event = Envelope.Create(new ContentCreated()); - - var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_no_action_handler_registered() - { - var @event = Envelope.Create(new ContentCreated()); - - var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_too_old() - { - var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); - - var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_not_triggered_with_precheck() - { - var rule = ValidRule(); - - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(false); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Null(job); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_create_job_if_enriched_event_not_created() - { - var rule = ValidRule(); - - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) - .Returns(Task.FromResult(null)); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Null(job); - } - - [Fact] - public async Task Should_not_create_job_if_not_triggered() - { - var rule = ValidRule(); - - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) - .Returns(enrichedEvent); - - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) - .Returns(false); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Null(job); - } - - [Fact] - public async Task Should_create_job_if_triggered() - { - var now = clock.GetCurrentInstant(); - - var rule = ValidRule(); - - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - - var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now); - - A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) - .Returns(enrichedEvent); - - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) - .Returns((actionDescription, new ValidData { Value = 10 })); - - var job = await sut.CreateJobAsync(rule, ruleId, @event); - - Assert.Equal(actionData, job.ActionData); - Assert.Equal(actionName, job.ActionName); - Assert.Equal(actionDescription, job.Description); - - Assert.Equal(now, job.Created); - Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires); - - Assert.Equal(enrichedEvent.AppId.Id, job.AppId); - - Assert.NotEqual(Guid.Empty, job.Id); - - A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A>.That.Matches(x => x.Payload == @event.Payload))) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Returns(Result.Success(actionDump)); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(RuleResult.Success, result.Result.Status); - - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Returns(Result.Failed(new InvalidOperationException(), actionDump)); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(RuleResult.Failed, result.Result.Status); - - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Returns(Result.Failed(new TimeoutException(), actionDump)); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(RuleResult.Timeout, result.Result.Status); - - Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.True(result.Result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - - Assert.True(result.Result.Dump.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); - } - - [Fact] - public async Task Should_create_exception_details_when_job_to_execute_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A.That.Matches(x => x.Value == 10), A.Ignored)) - .Throws(ex); - - var result = await sut.InvokeAsync(actionName, actionData); - - Assert.Equal(ex, result.Result.Exception); - } - - private static Rule RuleInvalidAction() - { - return new Rule(new ContentChangedTriggerV2(), new InvalidAction()); - } - - private static Rule RuleInvalidTrigger() - { - return new Rule(new InvalidTrigger(), new ValidAction()); - } - - private static Rule ValidRule() - { - return new Rule(new ContentChangedTriggerV2(), new ValidAction()); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs deleted file mode 100644 index 6032337d1..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.Tags -{ - public class TagNormalizerTests - { - private readonly ITagService tagService = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Schema schema; - - public TagNormalizerTests() - { - schema = - new Schema("my-schema") - .AddTags(1, "tags1", Partitioning.Invariant) - .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) - .AddString(3, "string", Partitioning.Invariant) - .AddArray(4, "array", Partitioning.Invariant, f => f - .AddTags(401, "nestedTags1") - .AddTags(402, "nestedTags2", new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) - .AddString(403, "string")); - } - - [Fact] - public async Task Should_normalize_tags_with_old_data() - { - var newData = GenerateData("n_raw"); - var oldData = GenerateData("o_raw"); - - A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), - A>.That.IsSameSequenceAs("n_raw2_1", "n_raw2_2", "n_raw4"), - A>.That.IsSameSequenceAs("o_raw2_1", "o_raw2_2", "o_raw4"))) - .Returns(new Dictionary - { - ["n_raw2_2"] = "id2_2", - ["n_raw2_1"] = "id2_1", - ["n_raw4"] = "id4" - }); - - await tagService.NormalizeAsync(appId, schemaId, schema, newData, oldData); - - Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]["iv"]); - Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); - } - - [Fact] - public async Task Should_normalize_tags_without_old_data() - { - var newData = GenerateData("name"); - - A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), - A>.That.IsSameSequenceAs("name2_1", "name2_2", "name4"), - A>.That.IsEmpty())) - .Returns(new Dictionary - { - ["name2_2"] = "id2_2", - ["name2_1"] = "id2_1", - ["name4"] = "id4" - }); - - await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); - - Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]["iv"]); - Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); - } - - [Fact] - public async Task Should_denormalize_tags() - { - var newData = GenerateData("id"); - - A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), - A>.That.IsSameSequenceAs("id2_1", "id2_2", "id4"), - A>.That.IsEmpty())) - .Returns(new Dictionary - { - ["id2_2"] = "name2_2", - ["id2_1"] = "name2_1", - ["id4"] = "name4" - }); - - await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); - - Assert.Equal(JsonValue.Array("name2_1", "name2_2"), newData["tags2"]["iv"]); - Assert.Equal(JsonValue.Array("name4"), GetNestedTags(newData)); - } - - private static IJsonValue GetNestedTags(NamedContentData newData) - { - var array = (JsonArray)newData["array"]["iv"]; - var arrayItem = (JsonObject)array[0]; - - return arrayItem["nestedTags2"]; - } - - private static NamedContentData GenerateData(string prefix) - { - return new NamedContentData() - .AddField("tags1", - new ContentFieldData() - .AddValue("iv", JsonValue.Array($"{prefix}1"))) - .AddField("tags2", - new ContentFieldData() - .AddValue("iv", JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) - .AddField("string", - new ContentFieldData() - .AddValue("iv", $"{prefix}stringValue")) - .AddField("array", - new ContentFieldData() - .AddValue("iv", - JsonValue.Array( - JsonValue.Object() - .Add("nestedTags1", JsonValue.Array($"{prefix}3")) - .Add("nestedTags2", JsonValue.Array($"{prefix}4")) - .Add("string", $"{prefix}nestedStringValue")))); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs deleted file mode 100644 index d2df5f606..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class ArrayFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new ArrayFieldProperties()); - - Assert.Equal("my-array", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_items_are_valid() - { - var sut = Field(new ArrayFieldProperties()); - - await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_items_are_null_and_valid() - { - var sut = Field(new ArrayFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_items_is_equal_to_min_and_max_items() - { - var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_items_are_required_and_null() - { - var sut = Field(new ArrayFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_items_are_required_and_empty() - { - var sut = Field(new ArrayFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new ArrayFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ArrayFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - private static IJsonValue CreateValue(params JsonObject[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); - } - - private static RootField Field(ArrayFieldProperties properties) - { - return Fields.Array(1, "my-array", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs deleted file mode 100644 index 5b1dfbe5b..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ /dev/null @@ -1,321 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class AssetsFieldTests - { - private readonly List errors = new List(); - - public sealed class AssetInfo : IAssetInfo - { - public Guid AssetId { get; set; } - - public string FileName { get; set; } - - public string FileHash { get; set; } - - public string Slug { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } - } - - private readonly AssetInfo document = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyDocument.pdf", - FileSize = 1024 * 4, - IsImage = false, - PixelWidth = null, - PixelHeight = null - }; - - private readonly AssetInfo image1 = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyImage.png", - FileSize = 1024 * 8, - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 - }; - - private readonly AssetInfo image2 = new AssetInfo - { - AssetId = Guid.NewGuid(), - FileName = "MyImage.png", - FileSize = 1024 * 8, - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 - }; - - private readonly ValidationContext ctx; - - public AssetsFieldTests() - { - ctx = ValidationTestExtensions.Assets(image1, image2, document); - } - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new AssetsFieldProperties()); - - Assert.Equal("my-assets", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_assets_are_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(document.AssetId), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_assets_are_null_and_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_ignored() - { - var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_assets_are_required_and_null() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_assets_are_required_and_empty() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_asset_are_not_valid() - { - var assetId = Guid.NewGuid(); - - var sut = Field(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(assetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { $"[1]: Id '{assetId}' not found." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." }); - } - - [Fact] - public async Task Should_add_error_if_document_is_not_an_image() - { - var sut = Field(new AssetsFieldProperties { MustBeImage = true }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Not an image." }); - } - - [Fact] - public async Task Should_add_error_if_values_contains_duplicate() - { - var sut = Field(new AssetsFieldProperties { MustBeImage = true }); - - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } - - [Fact] - public async Task Should_add_error_if_image_width_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinWidth = 1000 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_width_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxWidth = 700 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_height_is_too_small() - { - var sut = Field(new AssetsFieldProperties { MinHeight = 800 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_height_is_too_big() - { - var sut = Field(new AssetsFieldProperties { MaxHeight = 500 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." }); - } - - [Fact] - public async Task Should_add_error_if_image_has_invalid_aspect_ratio() - { - var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] { "[2]: Aspect ratio not '1:1'." }); - } - - [Fact] - public async Task Should_add_error_if_image_has_invalid_extension() - { - var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") }); - - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx); - - errors.Should().BeEquivalentTo( - new[] - { - "[1]: Invalid file extension.", - "[2]: Invalid file extension." - }); - } - - private static IJsonValue CreateValue(params Guid[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - - private static RootField Field(AssetsFieldProperties properties) - { - return Fields.Assets(1, "my-assets", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs deleted file mode 100644 index e3d21769d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ /dev/null @@ -1,192 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class ReferencesFieldTests - { - private readonly List errors = new List(); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid ref1 = Guid.NewGuid(); - private readonly Guid ref2 = Guid.NewGuid(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new ReferencesFieldProperties()); - - Assert.Equal("my-refs", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_references_are_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(ref1), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_references_are_null_and_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_allowed() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); - - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_not_defined() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_references_are_required_and_null() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_references_are_required_and_empty() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_reference_are_not_valid() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References()); - - errors.Should().BeEquivalentTo( - new[] { $"Contains invalid reference '{ref1}'." }); - } - - [Fact] - public async Task Should_add_error_if_reference_schema_is_not_valid() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); - - errors.Should().BeEquivalentTo( - new[] { $"Contains reference '{ref1}' to invalid schema." }); - } - - [Fact] - public async Task Should_add_error_if_reference_contains_duplicate_values() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, - ValidationTestExtensions.References( - (schemaId, ref1))); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } - - private static IJsonValue CreateValue(params Guid[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - - private ValidationContext Context() - { - return ValidationTestExtensions.References( - (schemaId, ref1), - (schemaId, ref2)); - } - - private static RootField Field(ReferencesFieldProperties properties) - { - return Fields.References(1, "my-refs", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs deleted file mode 100644 index db99fcede..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class StringFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new StringFieldProperties()); - - Assert.Equal("my-string", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_string_is_valid() - { - var sut = Field(new StringFieldProperties { Label = "" }); - - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_string_is_required_but_null() - { - var sut = Field(new StringFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_string_is_required_but_empty() - { - var sut = Field(new StringFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(string.Empty), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_string_is_shorter_than_min_length() - { - var sut = Field(new StringFieldProperties { MinLength = 10 }); - - await sut.ValidateAsync(CreateValue("123"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 10 character(s)." }); - } - - [Fact] - public async Task Should_add_error_if_string_is_longer_than_max_length() - { - var sut = Field(new StringFieldProperties { MaxLength = 5 }); - - await sut.ValidateAsync(CreateValue("12345678"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 5 character(s)." }); - } - - [Fact] - public async Task Should_add_error_if_string_not_allowed() - { - var sut = Field(new StringFieldProperties { AllowedValues = ReadOnlyCollection.Create("Foo") }); - - await sut.ValidateAsync(CreateValue("Bar"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not an allowed value." }); - } - - [Fact] - public async Task Should_add_error_if_number_is_not_valid_pattern() - { - var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); - - await sut.ValidateAsync(CreateValue("abc"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Does not match to the pattern." }); - } - - [Fact] - public async Task Should_add_error_if_number_is_not_valid_pattern_with_message() - { - var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); - - await sut.ValidateAsync(CreateValue("abc"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Custom Error Message." }); - } - - [Fact] - public async Task Should_add_error_if_unique_constraint_failed() - { - var sut = Field(new StringFieldProperties { IsUnique = true }); - - await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); - - errors.Should().BeEquivalentTo( - new[] { "Another content with the same value exists." }); - } - - private static IJsonValue CreateValue(string v) - { - return JsonValue.Create(v); - } - - private static RootField Field(StringFieldProperties properties) - { - return Fields.String(1, "my-string", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs deleted file mode 100644 index bab3782b2..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json.Objects; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class TagsFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new TagsFieldProperties()); - - Assert.Equal("my-tags", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_tags_are_valid() - { - var sut = Field(new TagsFieldProperties()); - - await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_tags_are_null_and_valid() - { - var sut = Field(new TagsFieldProperties()); - - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_tags_is_equal_to_min_and_max_items() - { - var sut = Field(new TagsFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue("tag1", "tag2"), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_tags_are_required_but_null() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_tags_are_required_but_empty() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_tag_value_is_null() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(JsonValue.Array(JsonValue.Null), errors); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_tag_value_is_empty() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(string.Empty), errors); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new TagsFieldProperties()); - - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Not a valid value." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new TagsFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new TagsFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_contains_an_not_allowed_values() - { - var sut = Field(new TagsFieldProperties { AllowedValues = ReadOnlyCollection.Create("tag-2", "tag-3") }); - - await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors); - - errors.Should().BeEquivalentTo( - new[] { "[1]: Not an allowed value." }); - } - - private static IJsonValue CreateValue(params string[] ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); - } - - private static RootField Field(TagsFieldProperties properties) - { - return Fields.Tags(1, "my-tags", Partitioning.Invariant, properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs deleted file mode 100644 index bb22186fe..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public class UIFieldTests - { - private readonly List errors = new List(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new UIFieldProperties()); - - Assert.Equal("my-ui", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_value_is_undefined() - { - var sut = Field(new UIFieldProperties()); - - await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_value_is_json_null() - { - var sut = Field(new UIFieldProperties()); - - await sut.ValidateAsync(JsonValue.Null, errors); - - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_valid() - { - var sut = Field(new UIFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(JsonValue.True, errors); - - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } - - [Fact] - public async Task Should_add_error_if_field_object_is_defined() - { - var schema = - new Schema("my-schema") - .AddUI(1, "my-ui1", Partitioning.Invariant) - .AddUI(2, "my-ui2", Partitioning.Invariant); - - var data = - new NamedContentData() - .AddField("my-ui1", new ContentFieldData()) - .AddField("my-ui2", new ContentFieldData() - .AddValue("iv", null)); - - var validationContext = ValidationTestExtensions.ValidContext; - var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); - - await validator.ValidateAsync(data); - - validator.Errors.Should().BeEquivalentTo( - new[] - { - new ValidationError("Value must not be defined.", "my-ui1"), - new ValidationError("Value must not be defined.", "my-ui2") - }); - } - - [Fact] - public async Task Should_add_error_if_array_item_field_is_defined() - { - var schema = - new Schema("my-schema") - .AddArray(1, "my-array", Partitioning.Invariant, array => array - .AddUI(101, "my-ui")); - - var data = - new NamedContentData() - .AddField("my-array", new ContentFieldData() - .AddValue("iv", - JsonValue.Array( - JsonValue.Object() - .Add("my-ui", null)))); - - var validationContext = - new ValidationContext( - Guid.NewGuid(), - Guid.NewGuid(), - (c, s) => null, - (s) => null, - (c) => null); - - var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); - - await validator.ValidateAsync(data); - - validator.Errors.Should().BeEquivalentTo( - new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); - } - - private static NestedField Field(UIFieldProperties properties) - { - return new NestedField(1, "my-ui", properties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs deleted file mode 100644 index 66885c301..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Core.ValidateContent.Validators; - -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public static class ValidationTestExtensions - { - private static readonly Task> EmptyReferences = Task.FromResult>(new List<(Guid SchemaId, Guid Id)>()); - private static readonly Task> EmptyAssets = Task.FromResult>(new List()); - - public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), - (x, y) => EmptyReferences, - (x) => EmptyReferences, - (x) => EmptyAssets); - - public static Task ValidateAsync(this IValidator validator, object value, IList errors, ValidationContext context = null) - { - return validator.ValidateAsync(value, - CreateContext(context), - CreateFormatter(errors)); - } - - public static Task ValidateOptionalAsync(this IValidator validator, object value, IList errors, ValidationContext context = null) - { - return validator.ValidateAsync( - value, - CreateContext(context).Optional(true), - CreateFormatter(errors)); - } - - public static Task ValidateAsync(this IField field, object value, IList errors, ValidationContext context = null) - { - return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) - .ValidateAsync( - value, - CreateContext(context), - CreateFormatter(errors)); - } - - private static AddError CreateFormatter(IList errors) - { - return (field, message) => - { - if (field == null || !field.Any()) - { - errors.Add(message); - } - else - { - errors.Add($"{field.ToPathString()}: {message}"); - } - }; - } - - private static ValidationContext CreateContext(ValidationContext context) - { - return context ?? ValidContext; - } - - public static ValidationContext Assets(params IAssetInfo[] assets) - { - var actual = Task.FromResult>(assets.ToList()); - - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); - } - - public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) - { - var actual = Task.FromResult>(referencesIds.ToList()); - - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj deleted file mode 100644 index ce93b49d2..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Domain.Apps.Core - 7.3 - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs deleted file mode 100644 index 2fb59c93e..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs +++ /dev/null @@ -1,173 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Domain.Apps.Core.Apps.Json; -using Squidex.Domain.Apps.Core.Contents.Json; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; -using Xunit; - -namespace Squidex.Domain.Apps.Core -{ - public static class TestUtils - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - - public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto) - { - var typeNameRegistry = - new TypeNameRegistry() - .Map(new FieldRegistry()) - .Map(new RuleRegistry()) - .MapUnmapped(typeof(TestUtils).Assembly); - - var serializerSettings = new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), - - ContractResolver = new ConverterContractResolver( - new AppClientsConverter(), - new AppContributorsConverter(), - new AppPatternsConverter(), - new ClaimsPrincipalConverter(), - new ContentFieldDataConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new InstantConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new LanguagesConfigConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new RolesConverter(), - new RuleConverter(), - new SchemaConverter(), - new StatusConverter(), - new StringEnumConverter(), - new WorkflowConverter(), - new WorkflowTransitionConverter()), - - TypeNameHandling = typeNameHandling - }; - - return new NewtonsoftJsonSerializer(serializerSettings); - } - - public static Schema MixedSchema(bool isSingleton = false) - { - var schema = new Schema("user", isSingleton: isSingleton) - .Publish() - .AddArray(101, "root-array", Partitioning.Language, f => f - .AddAssets(201, "nested-assets") - .AddBoolean(202, "nested-boolean") - .AddDateTime(203, "nested-datetime") - .AddGeolocation(204, "nested-geolocation") - .AddJson(205, "nested-json") - .AddJson(211, "nested-json2") - .AddNumber(206, "nested-number") - .AddReferences(207, "nested-references") - .AddString(208, "nested-string") - .AddTags(209, "nested-tags") - .AddUI(210, "nested-ui")) - .AddAssets(102, "root-assets", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddBoolean(103, "root-boolean", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddDateTime(104, "root-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) - .AddDateTime(105, "root-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) - .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties()) - .AddJson(107, "root-json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddNumber(108, "root-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) - .AddReferences(109, "root-references", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddString(110, "root-string1", Partitioning.Invariant, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadOnlyCollection.Create("a", "b") }) - .AddString(111, "root-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" }) - .AddTags(112, "root-tags", Partitioning.Language, - new TagsFieldProperties()) - .AddUI(113, "root-ui", Partitioning.Language, - new UIFieldProperties()) - .Update(new SchemaProperties { Hints = "The User" }) - .HideField(104) - .HideField(211, 101) - .DisableField(109) - .DisableField(212, 101) - .LockField(105); - - return schema; - } - - public static T SerializeAndDeserialize(this T value) - { - return DefaultSerializer.Deserialize(DefaultSerializer.Serialize(value)); - } - - public static void TestFreeze(IFreezable sut) - { - var properties = - sut.GetType().GetRuntimeProperties() - .Where(x => - x.CanWrite && - x.CanRead && - x.Name != "IsFrozen"); - - foreach (var property in properties) - { - var value = - property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; - - property.SetValue(sut, value); - - var result = property.GetValue(sut); - - Assert.Equal(value, result); - } - - sut.Freeze(); - - foreach (var property in properties) - { - var value = - property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; - - Assert.Throws(() => - { - try - { - property.SetValue(sut, value); - } - catch (Exception ex) - { - throw ex.InnerException; - } - }); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs deleted file mode 100644 index 69087e8d7..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable IDE0067 // Dispose objects before losing scope - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppCommandMiddlewareTests : HandlerTestBase - { - private readonly IContextProvider contextProvider = A.Fake(); - private readonly IAssetStore assetStore = A.Fake(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Context requestContext = Context.Anonymous(); - private readonly AppCommandMiddleware sut; - - public sealed class MyCommand : SquidexCommand - { - } - - protected override Guid Id - { - get { return appId; } - } - - public AppCommandMiddlewareTests() - { - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); - - sut = new AppCommandMiddleware(A.Fake(), assetStore, assetThumbnailGenerator, contextProvider); - } - - [Fact] - public async Task Should_replace_context_app_with_grain_result() - { - var result = A.Fake(); - - var command = CreateCommand(new MyCommand()); - var context = CreateContextForCommand(command); - - context.Complete(result); - - await sut.HandleAsync(context); - - Assert.Same(result, requestContext.App); - } - - [Fact] - public async Task Should_upload_image_to_store() - { - var stream = new MemoryStream(); - - var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); - - var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); - var context = CreateContextForCommand(command); - - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(new ImageInfo(100, 100)); - - await sut.HandleAsync(context); - - A.CallTo(() => assetStore.UploadAsync(appId.ToString(), stream, true, A.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_throw_exception_when_file_to_upload_is_not_an_image() - { - var stream = new MemoryStream(); - - var file = new AssetFile("name.jpg", "image/jpg", 1024, () => stream); - - var command = CreateCommand(new UploadAppImage { AppId = appId, File = file }); - var context = CreateContextForCommand(command); - - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(Task.FromResult(null)); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs deleted file mode 100644 index 36d8ef06a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ /dev/null @@ -1,658 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppGrainTests : HandlerTestBase - { - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); - private readonly IUser user = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientId = "client"; - private readonly string clientNewName = "My Client"; - private readonly string roleName = "My Role"; - private readonly string planIdPaid = "premium"; - private readonly string planIdFree = "free"; - private readonly AppGrain sut; - private readonly Guid workflowId = Guid.NewGuid(); - private readonly Guid patternId1 = Guid.NewGuid(); - private readonly Guid patternId2 = Guid.NewGuid(); - private readonly Guid patternId3 = Guid.NewGuid(); - private readonly InitialPatterns initialPatterns; - - protected override Guid Id - { - get { return AppId; } - } - - public AppGrainTests() - { - A.CallTo(() => user.Id) - .Returns(contributorId); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId)) - .Returns(user); - - A.CallTo(() => appPlansProvider.GetPlan(A.Ignored)) - .Returns(new ConfigAppLimitsPlan { MaxContributors = 10 }); - - initialPatterns = new InitialPatterns - { - { patternId1, new AppPattern("Number", "[0-9]") }, - { patternId2, new AppPattern("Numbers", "[0-9]*") } - }; - - sut = new AppGrain(initialPatterns, Store, A.Dummy(), appPlansProvider, appPlansBillingManager, userResolver); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public async Task Command_should_throw_exception_if_app_is_archived() - { - await ExecuteCreateAsync(); - await ExecuteArchiveAsync(); - - await Assert.ThrowsAsync(ExecuteAttachClientAsync); - } - - [Fact] - public async Task Create_should_create_events_and_update_state() - { - var command = new CreateApp { Name = AppName, Actor = Actor, AppId = AppId }; - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(AppName, sut.Snapshot.Name); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }), - CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }), - CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), - CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) - ); - } - - [Fact] - public async Task Update_should_create_events_and_update_state() - { - var command = new UpdateApp { Label = "my-label", Description = "my-description" }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal("my-label", sut.Snapshot.Label); - Assert.Equal("my-description", sut.Snapshot.Description); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppUpdated { Label = "my-label", Description = "my-description" }) - ); - } - - [Fact] - public async Task UploadImage_should_create_events_and_update_state() - { - var command = new UploadAppImage { File = new AssetFile("image.png", "image/png", 100, () => null) }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal("image/png", sut.Snapshot.Image.MimeType); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppImageUploaded { Image = sut.Snapshot.Image }) - ); - } - - [Fact] - public async Task RemoveImage_should_create_events_and_update_state() - { - var command = new RemoveAppImage(); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Null(sut.Snapshot.Image); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppImageRemoved()) - ); - } - - [Fact] - public async Task ChangePlan_should_create_events_and_update_state() - { - var command = new ChangePlan { PlanId = planIdPaid }; - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .Returns(new PlanChangedResult()); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - Assert.True(result.Value is PlanChangedResult); - - Assert.Equal(planIdPaid, sut.Snapshot.Plan.PlanId); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) - ); - } - - [Fact] - public async Task ChangePlan_should_reset_plan_for_reset_plan() - { - var command = new ChangePlan { PlanId = planIdFree }; - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .Returns(new PlanChangedResult()); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdFree)) - .Returns(new PlanResetResult()); - - await ExecuteCreateAsync(); - await ExecuteChangePlanAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - Assert.True(result.Value is PlanResetResult); - - Assert.Null(sut.Snapshot.Plan); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanReset()) - ); - } - - [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_result() - { - var command = new ChangePlan { PlanId = planIdPaid }; - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - - Assert.Null(sut.Snapshot.Plan); - } - - [Fact] - public async Task ChangePlan_should_not_call_billing_manager_for_callback() - { - var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(5)); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task AssignContributor_should_create_events_and_update_state() - { - var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsAdded = true }) - ); - } - - [Fact] - public async Task AssignContributor_should_create_update_events_and_update_state() - { - var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner }; - - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner }) - ); - } - - [Fact] - public async Task RemoveContributor_should_create_events_and_update_state() - { - var command = new RemoveContributor { ContributorId = contributorId }; - - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) - ); - } - - [Fact] - public async Task AttachClient_should_create_events_and_update_state() - { - var command = new AttachClient { Id = clientId }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) - ); - } - - [Fact] - public async Task UpdateClient_should_create_events_and_update_state() - { - var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer }; - - await ExecuteCreateAsync(); - await ExecuteAttachClientAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer }) - ); - } - - [Fact] - public async Task RevokeClient_should_create_events_and_update_state() - { - var command = new RevokeClient { Id = clientId }; - - await ExecuteCreateAsync(); - await ExecuteAttachClientAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientRevoked { Id = clientId }) - ); - } - - [Fact] - public async Task AddWorkflow_should_create_events_and_update_state() - { - var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.NotEmpty(sut.Snapshot.Workflows); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) - ); - } - - [Fact] - public async Task UpdateWorkflow_should_create_events_and_update_state() - { - var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; - - await ExecuteCreateAsync(); - await ExecuteAddWorkflowAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.NotEmpty(sut.Snapshot.Workflows); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) - ); - } - - [Fact] - public async Task DeleteWorkflow_should_create_events_and_update_state() - { - var command = new DeleteWorkflow { WorkflowId = workflowId }; - - await ExecuteCreateAsync(); - await ExecuteAddWorkflowAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Empty(sut.Snapshot.Workflows); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) - ); - } - - [Fact] - public async Task AddLanguage_should_create_events_and_update_state() - { - var command = new AddLanguage { Language = Language.DE }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageAdded { Language = Language.DE }) - ); - } - - [Fact] - public async Task RemoveLanguage_should_create_events_and_update_state() - { - var command = new RemoveLanguage { Language = Language.DE }; - - await ExecuteCreateAsync(); - await ExecuteAddLanguageAsync(Language.DE); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageRemoved { Language = Language.DE }) - ); - } - - [Fact] - public async Task UpdateLanguage_should_create_events_and_update_state() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; - - await ExecuteCreateAsync(); - await ExecuteAddLanguageAsync(Language.DE); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) - ); - } - - [Fact] - public async Task AddRole_should_create_events_and_update_state() - { - var command = new AddRole { Name = roleName }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(1, sut.Snapshot.Roles.CustomCount); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleAdded { Name = roleName }) - ); - } - - [Fact] - public async Task DeleteRole_should_create_events_and_update_state() - { - var command = new DeleteRole { Name = roleName }; - - await ExecuteCreateAsync(); - await ExecuteAddRoleAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(0, sut.Snapshot.Roles.CustomCount); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleDeleted { Name = roleName }) - ); - } - - [Fact] - public async Task UpdateRole_should_create_events_and_update_state() - { - var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" } }; - - await ExecuteCreateAsync(); - await ExecuteAddRoleAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = new[] { "clients.read" } }) - ); - } - - [Fact] - public async Task AddPattern_should_create_events_and_update_state() - { - var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) - ); - } - - [Fact] - public async Task DeletePattern_should_create_events_and_update_state() - { - var command = new DeletePattern { PatternId = patternId3 }; - - await ExecuteCreateAsync(); - await ExecuteAddPatternAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPatternDeleted { PatternId = patternId3 }) - ); - } - - [Fact] - public async Task UpdatePattern_should_create_events_and_update_state() - { - var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; - - await ExecuteCreateAsync(); - await ExecuteAddPatternAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) - ); - } - - [Fact] - public async Task ArchiveApp_should_create_events_and_update_state() - { - var command = new ArchiveApp(); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(5)); - - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppArchived()) - ); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null)) - .MustHaveHappened(); - } - - private Task ExecuteAddPatternAsync() - { - return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" })); - } - - private Task ExecuteCreateAsync() - { - return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName })); - } - - private Task ExecuteAssignContributorAsync() - { - return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Role = Role.Editor })); - } - - private Task ExecuteAttachClientAsync() - { - return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId })); - } - - private Task ExecuteAddRoleAsync() - { - return sut.ExecuteAsync(CreateCommand(new AddRole { Name = roleName })); - } - - private Task ExecuteAddLanguageAsync(Language language) - { - return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); - } - - private Task ExecuteAddWorkflowAsync() - { - return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); - } - - private Task ExecuteChangePlanAsync() - { - return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); - } - - private Task ExecuteArchiveAsync() - { - return sut.ExecuteAsync(CreateCommand(new ArchiveApp())); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs deleted file mode 100644 index 547db7299..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Billing -{ - public class NoopAppPlanBillingManagerTests - { - private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); - - [Fact] - public void Should_not_have_portal() - { - Assert.False(sut.HasPortal); - } - - [Fact] - public async Task Should_do_nothing_when_changing_plan() - { - await sut.ChangePlanAsync(null, null, null); - } - - [Fact] - public async Task Should_not_return_portal_link() - { - Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs deleted file mode 100644 index da4322d27..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Users; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppContributorsTests - { - private readonly IUser user1 = A.Fake(); - private readonly IUser user2 = A.Fake(); - private readonly IUser user3 = A.Fake(); - private readonly IUserResolver users = A.Fake(); - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly AppContributors contributors_0 = AppContributors.Empty; - private readonly Roles roles = Roles.Empty; - - public GuardAppContributorsTests() - { - A.CallTo(() => user1.Id).Returns("1"); - A.CallTo(() => user2.Id).Returns("2"); - A.CallTo(() => user3.Id).Returns("3"); - - A.CallTo(() => users.FindByIdOrEmailAsync("1")).Returns(user1); - A.CallTo(() => users.FindByIdOrEmailAsync("2")).Returns(user2); - A.CallTo(() => users.FindByIdOrEmailAsync("3")).Returns(user3); - - A.CallTo(() => users.FindByIdOrEmailAsync("1@email.com")).Returns(user1); - A.CallTo(() => users.FindByIdOrEmailAsync("2@email.com")).Returns(user2); - A.CallTo(() => users.FindByIdOrEmailAsync("3@email.com")).Returns(user3); - - A.CallTo(() => users.FindByIdOrEmailAsync("notfound")) - .Returns(Task.FromResult(null)); - - A.CallTo(() => appPlan.MaxContributors) - .Returns(10); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() - { - var command = new AssignContributor(); - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), - new ValidationError("Contributor id is required.", "ContributorId")); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_role_not_valid() - { - var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan), - new ValidationError("Role is not a valid value.", "Role")); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_user_already_exists_with_same_role() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan), - new ValidationError("Contributor has already this role.", "Role")); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IsRestore = true }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - - await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_user_not_found() - { - var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; - - await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_user_is_actor() - { - var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") }; - - await Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan)); - } - - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_max_reached() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "3" }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); - - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan), - new ValidationError("You have reached the maximum number of contributors for your plan.")); - } - - [Fact] - public async Task CanAssign_assign_if_if_user_added_by_email() - { - var command = new AssignContributor { ContributorId = "1@email.com" }; - - await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); - - Assert.Equal("1", command.ContributorId); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_found() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(-1); - - var command = new AssignContributor { ContributorId = "1" }; - - await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_has_another_role() - { - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Developer); - - await GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("2", Role.Developer); - - await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); - } - - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_from_restore() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "3", IsRestore = true }; - - var contributors_1 = contributors_0.Assign("1", Role.Editor); - var contributors_2 = contributors_1.Assign("2", Role.Editor); - - await GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_id_is_null() - { - var command = new RemoveContributor(); - - ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command), - new ValidationError("Contributor id is required.", "ContributorId")); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_not_found() - { - var command = new RemoveContributor { ContributorId = "1" }; - - Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_is_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); - - ValidationAssert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command), - new ValidationError("Cannot remove the only owner.")); - } - - [Fact] - public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Owner); - - GuardAppContributors.CanRemove(contributors_2, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs deleted file mode 100644 index 88b7093ac..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppRolesTests - { - private readonly string roleName = "Role1"; - private readonly Roles roles_0 = Roles.Empty; - private readonly AppContributors contributors = AppContributors.Empty; - private readonly AppClients clients = AppClients.Empty; - - [Fact] - public void CanAdd_should_throw_exception_if_name_empty() - { - var command = new AddRole { Name = null }; - - ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_0, command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_exists() - { - var roles_1 = roles_0.Add(roleName); - - var command = new AddRole { Name = roleName }; - - ValidationAssert.Throws(() => GuardAppRoles.CanAdd(roles_1, command), - new ValidationError("A role with the same name already exists.")); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_command_is_valid() - { - var command = new AddRole { Name = roleName }; - - GuardAppRoles.CanAdd(roles_0, command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_name_empty() - { - var command = new DeleteRole { Name = null }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanDelete_should_throw_exception_if_role_not_found() - { - var command = new DeleteRole { Name = roleName }; - - Assert.Throws(() => GuardAppRoles.CanDelete(roles_0, command, contributors, clients)); - } - - [Fact] - public void CanDelete_should_throw_exception_if_contributor_found() - { - var roles_1 = roles_0.Add(roleName); - - var command = new DeleteRole { Name = roleName }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors.Assign("1", roleName), clients), - new ValidationError("Cannot remove a role when a contributor is assigned.")); - } - - [Fact] - public void CanDelete_should_throw_exception_if_client_found() - { - var roles_1 = roles_0.Add(roleName); - - var command = new DeleteRole { Name = roleName }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), - new ValidationError("Cannot remove a role when a client is assigned.")); - } - - [Fact] - public void CanDelete_should_throw_exception_if_default_role() - { - var roles_1 = roles_0.Add(Role.Developer); - - var command = new DeleteRole { Name = Role.Developer }; - - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients), - new ValidationError("Cannot delete a default role.")); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_command_is_valid() - { - var roles_1 = roles_0.Add(roleName); - - var command = new DeleteRole { Name = roleName }; - - GuardAppRoles.CanDelete(roles_1, command, contributors, clients); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_name_empty() - { - var roles_1 = roles_0.Add(roleName); - - var command = new UpdateRole { Name = null, Permissions = new[] { "P1" } }; - - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_permission_is_null() - { - var roles_1 = roles_0.Add(roleName); - - var command = new UpdateRole { Name = roleName, Permissions = null }; - - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), - new ValidationError("Permissions is required.", "Permissions")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_default_role() - { - var roles_1 = roles_0.Add(Role.Developer); - - var command = new UpdateRole { Name = Role.Developer, Permissions = new[] { "P1" } }; - - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), - new ValidationError("Cannot update a default role.")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_role_does_not_exists() - { - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - - Assert.Throws(() => GuardAppRoles.CanUpdate(roles_0, command)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() - { - var roles_1 = roles_0.Add(roleName); - - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - - GuardAppRoles.CanUpdate(roles_1, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs deleted file mode 100644 index 39513d134..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Validation; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppTests - { - private readonly IUserResolver users = A.Fake(); - private readonly IAppPlansProvider appPlans = A.Fake(); - private readonly IAppLimitsPlan basicPlan = A.Fake(); - private readonly IAppLimitsPlan freePlan = A.Fake(); - - public GuardAppTests() - { - A.CallTo(() => users.FindByIdOrEmailAsync(A.Ignored)) - .Returns(A.Dummy()); - - A.CallTo(() => appPlans.GetPlan("notfound")) - .Returns(null); - - A.CallTo(() => appPlans.GetPlan("basic")) - .Returns(basicPlan); - - A.CallTo(() => appPlans.GetPlan("free")) - .Returns(freePlan); - } - - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateApp { Name = "INVALID NAME" }; - - ValidationAssert.Throws(() => GuardApp.CanCreate(command), - new ValidationError("Name is not a valid slug.", "Name")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_app_name_is_valid() - { - var command = new CreateApp { Name = "new-app" }; - - GuardApp.CanCreate(command); - } - - [Fact] - public void CanUploadImage_should_throw_exception_if_name_not_valid() - { - var command = new UploadAppImage(); - - ValidationAssert.Throws(() => GuardApp.CanUploadImage(command), - new ValidationError("File is required.", "File")); - } - - [Fact] - public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() - { - var command = new UploadAppImage { File = new AssetFile("file.png", "image/png", 100, () => null) }; - - GuardApp.CanUploadImage(command); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_id_is_null() - { - var command = new ChangePlan { Actor = new RefToken("user", "me") }; - - AppPlan plan = null; - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("Plan id is required.", "PlanId")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_not_found() - { - var command = new ChangePlan { PlanId = "notfound", Actor = new RefToken("user", "me") }; - - AppPlan plan = null; - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("A plan with this id does not exist.", "PlanId")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() - { - var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(new RefToken("user", "other"), "premium"); - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("Plan can only changed from the user who configured the plan initially.")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_is_the_same() - { - var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(command.Actor, "basic"); - - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans), - new ValidationError("App has already this plan.")); - } - - [Fact] - public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() - { - var command = new ChangePlan { PlanId = "basic", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(command.Actor, "premium"); - - GuardApp.CanChangePlan(command, plan, appPlans); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs deleted file mode 100644 index b22b3e030..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Guards -{ - public class GuardAppWorkflowTests - { - private readonly Guid workflowId = Guid.NewGuid(); - private readonly Workflows workflows; - - public GuardAppWorkflowTests() - { - workflows = Workflows.Empty.Add(workflowId, "name"); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_is_not_defined() - { - var command = new AddWorkflow(); - - ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_command_is_valid() - { - var command = new AddWorkflow { Name = "my-workflow" }; - - GuardAppWorkflows.CanAdd(command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_not_found() - { - var command = new UpdateWorkflow - { - Workflow = Workflow.Empty, - WorkflowId = Guid.NewGuid() - }; - - Assert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command)); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() - { - var command = new UpdateWorkflow { WorkflowId = workflowId }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Workflow is required.", "Workflow")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - default, - new Dictionary - { - [Status.Published] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Initial step is required.", "Workflow.Initial")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_initial_step_is_published() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Published, - new Dictionary - { - [Status.Published] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Draft] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Workflow must have a published step.", "Workflow.Steps")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Published] = null, - [Status.Draft] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Step is required.", "Workflow.Steps.Published")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition() - }), - [Status.Draft] = new WorkflowStep() - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Draft] = - new WorkflowStep(), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = null - }) - }), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), - new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() - { - var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; - - GuardAppWorkflows.CanUpdate(workflows, command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_workflow_not_found() - { - var command = new DeleteWorkflow { WorkflowId = Guid.NewGuid() }; - - Assert.Throws(() => GuardAppWorkflows.CanDelete(workflows, command)); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_workflow_is_found() - { - var command = new DeleteWorkflow { WorkflowId = workflowId }; - - GuardAppWorkflows.CanDelete(workflows, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs deleted file mode 100644 index e253c5ddc..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ /dev/null @@ -1,387 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsIndexTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAppsByNameIndexGrain indexByName = A.Fake(); - private readonly IAppsByUserIndexGrain indexByUser = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly string userId = "user-1"; - private readonly AppsIndex sut; - - public AppsIndexTests() - { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(indexByName); - - A.CallTo(() => grainFactory.GetGrain(userId, null)) - .Returns(indexByUser); - - sut = new AppsIndex(grainFactory); - } - - [Fact] - public async Task Should_resolve_all_apps_from_user_permissions() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new string[] { appId.Name }))) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_all_apps_from_user() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByUser.GetIdsAsync()) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_all_apps() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByName.GetIdsAsync()) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsAsync(); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_app_by_name() - { - var expected = SetupApp(0, false); - - A.CallTo(() => indexByName.GetIdAsync(appId.Name)) - .Returns(appId.Id); - - var actual = await sut.GetAppByNameAsync(appId.Name); - - Assert.Same(expected, actual); - } - - [Fact] - public async Task Should_resolve_app_by_id() - { - var expected = SetupApp(0, false); - - var actual = await sut.GetAppAsync(appId.Id); - - Assert.Same(expected, actual); - } - - [Fact] - public async Task Should_return_null_if_app_archived() - { - SetupApp(0, true); - - var actual = await sut.GetAppAsync(appId.Id); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_return_null_if_app_not_created() - { - SetupApp(-1, false); - - var actual = await sut.GetAppAsync(appId.Id); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_add_app_to_indexes_on_create() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var context = - new CommandContext(Create(appId.Name), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_app_to_user_index_if_app_created_by_client() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var context = - new CommandContext(CreateFromClient(appId.Name), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_clear_reservation_when_app_creation_failed() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var context = - new CommandContext(CreateFromClient(appId.Name), commandBus); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_indexes_on_create_if_name_taken() - { - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(Task.FromResult(null)); - - var context = - new CommandContext(Create(appId.Name), commandBus) - .Complete(); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => indexByName.AddAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_indexes_on_create_if_name_invalid() - { - var context = - new CommandContext(Create("INVALID"), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_app_to_index_on_contributor_assignment() - { - var context = - new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_from_user_index_on_remove_of_contributor() - { - var context = - new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_app_from_indexes_on_archive() - { - var app = SetupApp(0, false); - - var context = - new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.RemoveAsync(appId.Id)) - .MustHaveHappened(); - - A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding_for_contributors1() - { - var apps = new HashSet(); - - await sut.RebuildByContributorsAsync(userId, apps); - - A.CallTo(() => indexByUser.RebuildAsync(apps)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding_for_contributors2() - { - var users = new HashSet { userId }; - - await sut.RebuildByContributorsAsync(appId.Id, users); - - A.CallTo(() => indexByUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding() - { - var apps = new Dictionary(); - - await sut.RebuildAsync(apps); - - A.CallTo(() => indexByName.RebuildAsync(apps)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_reserveration() - { - await sut.AddAsync("token"); - - A.CallTo(() => indexByName.AddAsync("token")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_remove_reservation() - { - await sut.RemoveReservationAsync("token"); - - A.CallTo(() => indexByName.RemoveReservationAsync("token")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_request_for_ids() - { - await sut.GetIdsAsync(); - - A.CallTo(() => indexByName.GetIdsAsync()) - .MustHaveHappened(); - } - - private IAppEntity SetupApp(long version, bool archived) - { - var appEntity = A.Fake(); - - A.CallTo(() => appEntity.Name) - .Returns(appId.Name); - A.CallTo(() => appEntity.Version) - .Returns(version); - A.CallTo(() => appEntity.IsArchived) - .Returns(archived); - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); - - var appGrain = A.Fake(); - - A.CallTo(() => appGrain.GetStateAsync()) - .Returns(J.Of(appEntity)); - - A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) - .Returns(appGrain); - - return appEntity; - } - - private CreateApp Create(string name) - { - return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorSubject() }; - } - - private CreateApp CreateFromClient(string name) - { - return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorClient() }; - } - - private RefToken ActorSubject() - { - return new RefToken(RefTokenType.Subject, userId); - } - - private RefToken ActorClient() - { - return new RefToken(RefTokenType.Client, userId); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs deleted file mode 100644 index bcb311f9a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public class AssetChangedTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IAssetLoader assetLoader = A.Fake(); - private readonly IRuleTriggerHandler sut; - - public AssetChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) - .Returns(true); - - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) - .Returns(false); - - sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); - } - - public static IEnumerable TestEvents = new[] - { - new object[] { new AssetCreated(), EnrichedAssetEventType.Created }, - new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }, - new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated }, - new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted } - }; - - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type) - { - var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - - A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12)) - .Returns(new AssetEntity()); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal(type, ((EnrichedAssetEvent)result).Type); - } - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_event_type_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new AssetCreated(), trigger, Guid.NewGuid()); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedContentEvent(), trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_is_empty() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_matchs() - { - TestForCondition("true", trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() - { - TestForCondition("false", trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.False(result); - }); - } - - private void TestForCondition(string condition, Action action) - { - var trigger = new AssetChangedTriggerV2 { Condition = condition }; - - action(trigger); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustHaveHappened(); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs deleted file mode 100644 index 272ba9c8c..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure.Assets; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets -{ - public class FileTypeTagGeneratorTests - { - private readonly HashSet tags = new HashSet(); - private readonly FileTypeTagGenerator sut = new FileTypeTagGenerator(); - - [Fact] - public void Should_not_add_tag_if_no_file_info() - { - var command = new CreateAsset(); - - sut.GenerateTags(command, tags); - - Assert.Empty(tags); - } - - [Fact] - public void Should_add_file_type() - { - var command = new CreateAsset - { - File = new AssetFile("File.DOCX", "Mime", 100, () => null) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("type/docx", tags); - } - - [Fact] - public void Should_add_blob_if_without_extension() - { - var command = new CreateAsset - { - File = new AssetFile("File", "Mime", 100, () => null) - }; - - sut.GenerateTags(command, tags); - - Assert.Contains("type/blob", tags); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs deleted file mode 100644 index 0a54832e7..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs +++ /dev/null @@ -1,236 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using FakeItEasy; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using NodaTime.Text; -using Squidex.Domain.Apps.Entities.MongoDb.Assets; -using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Validation; -using Xunit; -using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; -using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; - -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb -{ - public class MongoDbQueryTests - { - private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; - private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - - static MongoDbQueryTests() - { - InstantSerializer.Register(); - } - - [Fact] - public void Should_throw_exception_for_full_text_search() - { - Assert.Throws(() => Q(new ClrQuery { FullText = "Full Text" })); - } - - [Fact] - public void Should_make_query_with_lastModified() - { - var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_lastModifiedBy() - { - var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); - var o = C("{ 'mb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_created() - { - var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_createdBy() - { - var i = F(ClrFilter.Eq("createdBy", "Me")); - var o = C("{ 'cb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version() - { - var i = F(ClrFilter.Eq("version", 0)); - var o = C("{ 'vs' : NumberLong(0) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileVersion() - { - var i = F(ClrFilter.Eq("fileVersion", 2)); - var o = C("{ 'fv' : NumberLong(2) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_tags() - { - var i = F(ClrFilter.Eq("tags", "tag1")); - var o = C("{ 'td' : 'tag1' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileName() - { - var i = F(ClrFilter.Eq("fileName", "Logo.png")); - var o = C("{ 'fn' : 'Logo.png' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_isImage() - { - var i = F(ClrFilter.Eq("isImage", true)); - var o = C("{ 'im' : true }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_mimeType() - { - var i = F(ClrFilter.Eq("mimeType", "text/json")); - var o = C("{ 'mm' : 'text/json' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_fileSize() - { - var i = F(ClrFilter.Eq("fileSize", 1024)); - var o = C("{ 'fs' : NumberLong(1024) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_pixelHeight() - { - var i = F(ClrFilter.Eq("pixelHeight", 600)); - var o = C("{ 'ph' : 600 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_pixelWidth() - { - var i = F(ClrFilter.Eq("pixelWidth", 800)); - var o = C("{ 'pw' : 800 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = S(SortBuilder.Descending("lastModified")); - var o = C("{ 'mt' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var i = S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy")); - var o = C("{ 'mt' : 1, 'mb' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_take_statement() - { - var query = new ClrQuery { Take = 3 }; - var cursor = A.Fake>(); - - cursor.AssetTake(query.AdjustToModel()); - - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); - } - - [Fact] - public void Should_make_skip_statement() - { - var query = new ClrQuery { Skip = 3 }; - var cursor = A.Fake>(); - - cursor.AssetSkip(query.AdjustToModel()); - - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } - - private static string C(string value) - { - return value.Replace('\'', '"'); - } - - private static string F(FilterNode filter) - { - return Q(new ClrQuery { Filter = filter }); - } - - private static string S(params SortNode[] sorts) - { - var cursor = A.Fake>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>.Ignored)) - .Invokes((SortDefinition sortDefinition) => - { - i = sortDefinition.Render(Serializer, Registry).ToString(); - }); - - cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); - - return i; - } - - private static string Q(ClrQuery query) - { - var rendered = - query.AdjustToModel().BuildFilter(false).Filter - .Render(Serializer, Registry).ToString(); - - return rendered; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs deleted file mode 100644 index 373c252ed..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public class AssetLoaderTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAssetGrain grain = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly AssetLoader sut; - - public AssetLoaderTests() - { - A.CallTo(() => grainFactory.GetGrain(id, null)) - .Returns(grain); - - sut = new AssetLoader(grainFactory); - } - - [Fact] - public async Task Should_throw_exception_if_no_state_returned() - { - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(null)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_throw_exception_if_state_has_other_version() - { - var content = new AssetEntity { Version = 5 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_return_content_from_state() - { - var content = new AssetEntity { Version = 10 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - var result = await sut.GetAsync(id, 10); - - Assert.Same(content, result); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs deleted file mode 100644 index f0da996fd..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure.Queries; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Assets.Queries -{ - public class FilterTagTransformerTests - { - private readonly ITagService tagService = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - - [Fact] - public void Should_normalize_tags() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) - .Returns(new Dictionary { ["name1"] = "id1" }); - - var source = ClrFilter.Eq("tags", "name1"); - - var result = FilterTagTransformer.Transform(source, appId, tagService); - - Assert.Equal("tags == 'id1'", result.ToString()); - } - - [Fact] - public void Should_not_fail_when_tags_not_found() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("name1"))) - .Returns(new Dictionary()); - - var source = ClrFilter.Eq("tags", "name1"); - - var result = FilterTagTransformer.Transform(source, appId, tagService); - - Assert.Equal("tags == 'name1'", result.ToString()); - } - - [Fact] - public void Should_not_normalize_other_field() - { - var source = ClrFilter.Eq("other", "value"); - - var result = FilterTagTransformer.Transform(source, appId, tagService); - - Assert.Equal("other == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs deleted file mode 100644 index 980a44197..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private -#pragma warning disable RECS0070 - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentChangedTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IContentLoader contentLoader = A.Fake(); - private readonly IRuleTriggerHandler sut; - private readonly Guid ruleId = Guid.NewGuid(); - private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); - private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); - - public ContentChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) - .Returns(true); - - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) - .Returns(false); - - sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); - } - - public static IEnumerable TestEvents = new[] - { - new object[] { new ContentCreated(), EnrichedContentEventType.Created }, - new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, - new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, - new object[] { new ContentStatusChanged { Change = StatusChange.Change }, EnrichedContentEventType.StatusChanged }, - new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, - new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } - }; - - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type) - { - var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - - A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) - .Returns(new ContentEntity { SchemaId = SchemaMatch }); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal(type, ((EnrichedContentEvent)result).Type); - } - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new AssetCreated(), trigger, ruleId); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_precheck_when_trigger_contains_no_schemas() - { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_handling_all_events() - { - TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_condition_is_empty() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_precheck_when_schema_id_does_not_match() - { - TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_trigger_contains_no_schemas() - { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_check_when_handling_all_events() - { - TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_is_empty() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_matchs() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_schema_id_does_not_match() - { - TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() - { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => - { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); - - Assert.False(result); - }); - } - - private void TestForTrigger(bool handleAll, NamedId schemaId, string condition, Action action) - { - var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; - - if (schemaId != null) - { - trigger.Schemas = new ReadOnlyCollection(new List - { - new ContentChangedTriggerSchemaV2 - { - SchemaId = schemaId.Id, Condition = condition - } - }); - } - - action(trigger); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustHaveHappened(); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs deleted file mode 100644 index af8fc0bd6..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ /dev/null @@ -1,592 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentGrainTests : HandlerTestBase - { - private readonly Guid contentId = Guid.NewGuid(); - private readonly IActivationLimit limit = A.Fake(); - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake(); - private readonly IContentRepository contentRepository = A.Dummy(); - private readonly IContentWorkflow contentWorkflow = A.Fake(x => x.Wrapping(new DefaultContentWorkflow())); - private readonly ISchemaEntity schema; - private readonly IScriptEngine scriptEngine = A.Fake(); - - private readonly NamedContentData invalidData = - new NamedContentData() - .AddField("my-field1", - new ContentFieldData() - .AddValue("iv", null)) - .AddField("my-field2", - new ContentFieldData() - .AddValue("iv", 1)); - private readonly NamedContentData data = - new NamedContentData() - .AddField("my-field1", - new ContentFieldData() - .AddValue("iv", 1)); - private readonly NamedContentData patch = - new NamedContentData() - .AddField("my-field2", - new ContentFieldData() - .AddValue("iv", 2)); - private readonly NamedContentData otherData = - new NamedContentData() - .AddField("my-field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("my-field2", - new ContentFieldData() - .AddValue("iv", 2)); - private readonly NamedContentData patched; - private readonly ContentGrain sut; - - protected override Guid Id - { - get { return contentId; } - } - - public ContentGrainTests() - { - app = Mocks.App(AppNamedId, Language.DE); - - var scripts = new SchemaScripts - { - Change = "", - Create = "", - Delete = "", - Update = "" - }; - - var schemaDef = - new Schema("my-schema") - .AddNumber(1, "my-field1", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true }) - .AddNumber(2, "my-field2", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = false }) - .ConfigureScripts(scripts); - - schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); - - A.CallTo(() => appProvider.GetAppAsync(AppName)) - .Returns(app); - - A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) - .Returns((app, schema)); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .ReturnsLazily(x => x.GetArgument(0).Data); - - patched = patch.MergeInto(data); - - sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository, limit); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public void Should_set_limit() - { - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); - } - - [Fact] - public async Task Command_should_throw_exception_if_content_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); - - await Assert.ThrowsAsync(ExecuteUpdateAsync); - } - - [Fact] - public async Task Create_should_create_events_and_update_state() - { - var command = new CreateContent { Data = data }; - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Create_should_also_publish() - { - var command = new CreateContent { Data = data, Publish = true }; - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Published, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Create_should_throw_when_invalid_data_is_passed() - { - var command = new CreateContent { Data = invalidData }; - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); - } - - [Fact] - public async Task Update_should_create_events_and_update_state() - { - var command = new UpdateContent { Data = otherData }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_should_create_proposal_events_and_update_state() - { - var command = new UpdateContent { Data = otherData, AsDraft = true }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdateProposed { Data = otherData }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_should_not_create_event_for_same_data() - { - var command = new UpdateContent { Data = data }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Single(LastEvents); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_when_invalid_data_is_passed() - { - var command = new UpdateContent { Data = invalidData }; - - await ExecuteCreateAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); - } - - [Fact] - public async Task Patch_should_create_events_and_update_state() - { - var command = new PatchContent { Data = patch }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = patched }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_create_proposal_events_and_update_state() - { - var command = new PatchContent { Data = patch, AsDraft = true }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.True(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdateProposed { Data = patched }) - ); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_not_create_event_for_same_data() - { - var command = new PatchContent { Data = data }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Single(LastEvents); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state() - { - var command = new ChangeContentStatus { Status = Status.Published }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Published, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state_when_archived() - { - var command = new ChangeContentStatus { Status = Status.Archived }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Archived, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state_when_unpublished() - { - var command = new ChangeContentStatus { Status = Status.Draft }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Unpublished, Status = Status.Draft }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Published), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_events_and_update_state_when_restored() - { - var command = new ChangeContentStatus { Status = Status.Draft }; - - await ExecuteCreateAsync(); - await ExecuteArchiveAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Archived), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_create_proposal_events_and_update_state() - { - var command = new ChangeContentStatus { Status = Status.Published }; - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteProposeUpdateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.False(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentChangesPublished()) - ); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() - { - var dueTime = Instant.MaxValue; - - var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob.Status); - Assert.Equal(dueTime, sut.Snapshot.ScheduleJob.DueTime); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) - ); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_refresh_properties_and_revert_scheduling_when_invoked_by_scheduler() - { - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Published, Instant.MaxValue); - - var command = new ChangeContentStatus { Status = Status.Published, JobId = sut.Snapshot.ScheduleJob.Id }; - - A.CallTo(() => contentWorkflow.CanMoveToAsync(A.Ignored, Status.Published, User)) - .Returns(false); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(sut.Snapshot); - - Assert.Null(sut.Snapshot.ScheduleJob); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentSchedulingCancelled()) - ); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Delete_should_update_properties_and_create_events() - { - var command = new DeleteContent(); - - await ExecuteCreateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(1)); - - Assert.True(sut.Snapshot.IsDeleted); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) - ); - - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft), "")) - .MustHaveHappened(); - } - - [Fact] - public async Task DiscardChanges_should_update_properties_and_create_events() - { - var command = new DiscardChanges(); - - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteProposeUpdateAsync(); - - var result = await sut.ExecuteAsync(CreateContentCommand(command)); - - result.ShouldBeEquivalent(new EntitySavedResult(3)); - - Assert.False(sut.Snapshot.IsPending); - - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentChangesDiscarded()) - ); - } - - private Task ExecuteCreateAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data })); - } - - private Task ExecuteUpdateAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData })); - } - - private Task ExecuteProposeUpdateAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true })); - } - - private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) - { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime })); - } - - private Task ExecuteDeleteAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new DeleteContent())); - } - - private Task ExecuteArchiveAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Archived })); - } - - private Task ExecutePublishAsync() - { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - } - - private ScriptContext ScriptContext(NamedContentData newData, NamedContentData oldData, Status newStatus) - { - return A.That.Matches(x => M(x, newData, oldData, newStatus, default)); - } - - private ScriptContext ScriptContext(NamedContentData newData, NamedContentData oldData, Status newStatus, Status oldStatus) - { - return A.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus)); - } - - private bool M(ScriptContext x, NamedContentData newData, NamedContentData oldData, Status newStatus, Status oldStatus) - { - return - Equals(x.Data, newData) && - Equals(x.DataOld, oldData) && - Equals(x.Status, newStatus) && - Equals(x.StatusOld, oldStatus) && - x.ContentId == contentId && x.User == User; - } - - protected T CreateContentEvent(T @event) where T : ContentEvent - { - @event.ContentId = contentId; - - return CreateEvent(@event); - } - - protected T CreateContentCommand(T command) where T : ContentCommand - { - command.ContentId = contentId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs deleted file mode 100644 index a738581b2..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class DefaultContentWorkflowTests - { - private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); - - [Fact] - public async Task Should_always_allow_publish_on_create() - { - var result = await sut.CanPublishOnCreateAsync(null, null, null); - - Assert.True(result); - } - - [Fact] - public async Task Should_draft_as_initial_status() - { - var expected = new StatusInfo(Status.Draft, StatusColors.Draft); - - var result = await sut.GetInitialStatusAsync(null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_check_is_valid_next() - { - var content = new ContentEntity { Status = Status.Published }; - - var result = await sut.CanMoveToAsync(content, Status.Draft, null); - - Assert.True(result); - } - - [Fact] - public async Task Should_be_able_to_update_published() - { - var content = new ContentEntity { Status = Status.Published }; - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_be_able_to_update_draft() - { - var content = new ContentEntity { Status = Status.Published }; - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_be_able_to_update_archived() - { - var content = new ContentEntity { Status = Status.Archived }; - - var result = await sut.CanUpdateAsync(content); - - Assert.False(result); - } - - [Fact] - public async Task Should_get_next_statuses_for_draft() - { - var content = new ContentEntity { Status = Status.Draft }; - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_archived() - { - var content = new ContentEntity { Status = Status.Archived }; - - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_published() - { - var content = new ContentEntity { Status = Status.Published }; - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses() - { - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(null); - - result.Should().BeEquivalentTo(expected); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs deleted file mode 100644 index 2159344b4..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class DefaultWorkflowsValidatorTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly DefaultWorkflowsValidator sut; - - public DefaultWorkflowsValidatorTests() - { - var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) - .Returns(Task.FromResult(null)); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(schema); - - sut = new DefaultWorkflowsValidator(appProvider); - } - - [Fact] - public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() - { - var workflows = Workflows.Empty - .Add(Guid.NewGuid(), "workflow1") - .Add(Guid.NewGuid(), "workflow2"); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Equal(errors, new[] { "Multiple workflows cover all schemas." }); - } - - [Fact] - public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })) - .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Equal(errors, new[] { "The schema `my-schema` is covered by multiple workflows." }); - } - - [Fact] - public async Task Should_not_generate_error_if_schema_deleted() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var oldSchemaId = Guid.NewGuid(); - - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })) - .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_generate_errors_for_no_overlaps() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); - - var errors = await sut.ValidateAsync(appId.Id, workflows); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_generate_errors_for_empty_workflows() - { - var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs deleted file mode 100644 index 73eb10962..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ /dev/null @@ -1,352 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class DynamicContentWorkflowTests - { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly NamedId simpleSchemaId = NamedId.Of(Guid.NewGuid(), "my-simple-schema"); - private readonly DynamicContentWorkflow sut; - - private readonly Workflow workflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Archived] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Archived, true), - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Published] = new WorkflowTransition("data.field.iv === 2", ReadOnlyCollection.Create("Owner", "Editor")) - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Published) - }); - - public DynamicContentWorkflowTests() - { - app = Mocks.App(appId); - - var simpleWorkflow = new Workflow( - Status.Draft, - new Dictionary - { - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Published] = new WorkflowTransition() - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Published) - }, - new List { simpleSchemaId.Id }); - - var workflows = Workflows.Empty.Set(workflow).Set(Guid.NewGuid(), simpleWorkflow); - - A.CallTo(() => appProvider.GetAppAsync(appId.Id)) - .Returns(app); - - A.CallTo(() => app.Workflows) - .Returns(workflows); - - sut = new DynamicContentWorkflow(new JintScriptEngine(), appProvider); - } - - [Fact] - public async Task Should_draft_as_initial_status() - { - var expected = new StatusInfo(Status.Draft, StatusColors.Draft); - - var result = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_allow_publish_on_create() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_allow_publish_on_create_if_data_is_invalid() - { - var content = CreateContent(Status.Draft, 4); - - var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Editor")); - - Assert.False(result); - } - - [Fact] - public async Task Should_not_allow_publish_on_create_if_role_not_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.DataDraft, Mocks.FrontendUser("Developer")); - - Assert.False(result); - } - - [Fact] - public async Task Should_check_is_valid_next() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_allow_transition_if_role_is_not_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Developer")); - - Assert.False(result); - } - - [Fact] - public async Task Should_allow_transition_if_role_is_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_allow_transition_if_data_not_valid() - { - var content = CreateContent(Status.Draft, 4); - - var result = await sut.CanMoveToAsync(content, Status.Published, Mocks.FrontendUser("Editor")); - - Assert.False(result); - } - - [Fact] - public async Task Should_be_able_to_update_published() - { - var content = CreateContent(Status.Published, 2); - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_be_able_to_update_draft() - { - var content = CreateContent(Status.Published, 2); - - var result = await sut.CanUpdateAsync(content); - - Assert.True(result); - } - - [Fact] - public async Task Should_not_be_able_to_update_archived() - { - var content = CreateContent(Status.Archived, 2); - - var result = await sut.CanUpdateAsync(content); - - Assert.False(result); - } - - [Fact] - public async Task Should_get_next_statuses_for_draft() - { - var content = CreateContent(Status.Draft, 2); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived) - }; - - var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Developer")); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_limit_next_statuses_if_expression_does_not_evauate_to_true() - { - var content = CreateContent(Status.Draft, 4); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived) - }; - - var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_limit_next_statuses_if_role_is_not_allowed() - { - var content = CreateContent(Status.Draft, 2); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetNextsAsync(content, Mocks.FrontendUser("Editor")); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_archived() - { - var content = CreateContent(Status.Archived, 2); - - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_get_next_statuses_for_published() - { - var content = CreateContent(Status.Published, 2); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft) - }; - - var result = await sut.GetNextsAsync(content, null); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses() - { - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses_for_simple_schema_workflow() - { - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_return_all_statuses_for_default_workflow_when_no_workflow_configured() - { - A.CallTo(() => app.Workflows).Returns(Workflows.Empty); - - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; - - var result = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - - result.Should().BeEquivalentTo(expected); - } - - private IContentEntity CreateContent(Status status, int value, bool simple = false) - { - var content = new ContentEntity { AppId = appId, Status = status }; - - if (simple) - { - content.SchemaId = simpleSchemaId; - } - else - { - content.SchemaId = schemaId; - } - - content.DataDraft = - new NamedContentData() - .AddField("field", - new ContentFieldData() - .AddValue("iv", value)); - - return content; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs deleted file mode 100644 index 78c2ad0c8..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ /dev/null @@ -1,1270 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public class GraphQLQueriesTests : GraphQLTestBase - { - [Fact] - public async Task Should_introspect() - { - const string query = @" - query IntrospectionQuery { - __schema { - queryType { name } - mutationType { name } - subscriptionType { name } - types { - ...FullType - } - directives { - name - description - args { - ...InputValue - } - onOperation - onFragment - onField - } - } - } - - fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - - fragment InputValue on __InputValue { - name - description - type { ...TypeRef } - defaultValue - } - - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - }"; - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); - - var json = serializer.Serialize(result.Response, true); - - Assert.NotEmpty(json); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_empty_object_for_empty_query(string query) - { - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_assets_when_querying_assets() - { - const string query = @" - query { - queryAssets(filter: ""my-query"", top: 30, skip: 5) { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileHash - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - tags - slug - } - }"; - - var asset = CreateAsset(Guid.NewGuid()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) - .Returns(ResultList.CreateFrom(0, asset)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryAssets = new dynamic[] - { - new - { - id = asset.Id, - version = 1, - created = asset.Created, - createdBy = "subject:user1", - lastModified = asset.LastModified, - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileHash = "ABC123", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, - slug = "myfile.png" - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_assets_with_total_when_querying_assets_with_total() - { - const string query = @" - query { - queryAssetsWithTotal(filter: ""my-query"", top: 30, skip: 5) { - total - items { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileHash - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - tags - slug - } - } - }"; - - var asset = CreateAsset(Guid.NewGuid()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) - .Returns(ResultList.CreateFrom(10, asset)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryAssetsWithTotal = new - { - total = 10, - items = new dynamic[] - { - new - { - id = asset.Id, - version = 1, - created = asset.Created, - createdBy = "subject:user1", - lastModified = asset.LastModified, - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileHash = "ABC123", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, - slug = "myfile.png" - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_null_single_asset() - { - var assetId = Guid.NewGuid(); - - var query = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) - .Returns(ResultList.CreateFrom(1)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findAsset = (object)null - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_single_asset_when_finding_asset() - { - var assetId = Guid.NewGuid(); - var asset = CreateAsset(assetId); - - var query = @" - query { - findAsset(id: """") { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileHash - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - tags - slug - } - }".Replace("", assetId.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) - .Returns(ResultList.CreateFrom(1, asset)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findAsset = new - { - id = asset.Id, - version = 1, - created = asset.Created, - createdBy = "subject:user1", - lastModified = asset.LastModified, - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileHash = "ABC123", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600, - tags = new[] { "tag1", "tag2" }, - slug = "myfile.png" - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_contents_when_querying_contents() - { - const string query = @" - query { - queryMySchemaContents(top: 30, skip: 5) { - id - version - created - createdBy - lastModified - lastModifiedBy - status - statusColor - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - myLocalized { - de_DE - } - myArray { - iv { - nestedNumber - nestedBoolean - } - } - } - } - }"; - - var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) - .Returns(ResultList.CreateFrom(0, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryMySchemaContents = new dynamic[] - { - new - { - id = content.Id, - version = 1, - created = content.Created, - createdBy = "subject:user1", - lastModified = content.LastModified, - lastModifiedBy = "subject:user2", - status = "DRAFT", - statusColor = "red", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - myLocalized = new - { - de_DE = "de-DE" - }, - myArray = new - { - iv = new[] - { - new - { - nestedNumber = 10, - nestedBoolean = true - }, - new - { - nestedNumber = 20, - nestedBoolean = false - } - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_multiple_contents_with_total_when_querying_contents_with_total() - { - const string query = @" - query { - queryMySchemaContentsWithTotal(top: 30, skip: 5) { - total - items { - id - version - created - createdBy - lastModified - lastModifiedBy - status - statusColor - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - myLocalized { - de_DE - } - } - } - } - }"; - - var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) - .Returns(ResultList.CreateFrom(10, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryMySchemaContentsWithTotal = new - { - total = 10, - items = new dynamic[] - { - new - { - id = content.Id, - version = 1, - created = content.Created, - createdBy = "subject:user1", - lastModified = content.LastModified, - lastModifiedBy = "subject:user2", - status = "DRAFT", - statusColor = "red", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - myLocalized = new - { - de_DE = "de-DE" - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_single_content_with_duplicate_names() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - data { - myNumber { - iv - } - myNumber2 { - iv - } - myArray { - iv { - nestedNumber - nestedNumber2 - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - data = new - { - myNumber = new - { - iv = 1 - }, - myNumber2 = new - { - iv = 2 - }, - myArray = new - { - iv = new[] - { - new - { - nestedNumber = 10, - nestedNumber2 = 11 - }, - new - { - nestedNumber = 20, - nestedNumber2 = 21 - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_null_single_content() - { - var contentId = Guid.NewGuid(); - - var query = @" - query { - findMySchemaContent(id: """") { - id - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = (object)null - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_single_content_when_finding_content() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - id - version - created - createdBy - lastModified - lastModifiedBy - status - statusColor - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - myLocalized { - de_DE - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - version = 1, - created = content.Created, - createdBy = "subject:user1", - lastModified = content.LastModified, - lastModifiedBy = "subject:user2", - status = "DRAFT", - statusColor = "red", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - myLocalized = new - { - de_DE = "de-DE" - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() - { - var contentRefId = Guid.NewGuid(); - var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, contentRefId, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - id - data { - myReferences { - iv { - id - data { - ref1Field { - iv - } - } - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) - .Returns(ResultList.CreateFrom(0, contentRef)); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myReferences = new - { - iv = new[] - { - new - { - id = contentRefId, - data = new - { - ref1Field = new - { - iv = "ref1" - } - } - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() - { - var contentRefId = Guid.NewGuid(); - var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, contentRefId, Guid.Empty); - - var query = @" - query { - findMySchemaContent(id: """") { - id - data { - myUnion { - iv { - ... on Content { - id - } - ... on MyRefSchema1 { - data { - ref1Field { - iv - } - } - } - __typename - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) - .Returns(ResultList.CreateFrom(0, contentRef)); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myUnion = new - { - iv = new[] - { - new - { - id = contentRefId, - data = new - { - ref1Field = new - { - iv = "ref1" - } - }, - __typename = "MyRefSchema1" - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() - { - var assetRefId = Guid.NewGuid(); - var assetRef = CreateAsset(assetRefId); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, assetRefId); - - var query = @" - query { - findMySchemaContent(id: """") { - id - data { - myAssets { - iv { - id - } - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) - .Returns(ResultList.CreateFrom(0, assetRef)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myAssets = new - { - iv = new[] - { - new - { - id = assetRefId - } - } - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_make_multiple_queries() - { - var assetId1 = Guid.NewGuid(); - var assetId2 = Guid.NewGuid(); - var asset1 = CreateAsset(assetId1); - var asset2 = CreateAsset(assetId2); - - var query1 = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId1.ToString()); - var query2 = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId2.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1))) - .Returns(ResultList.CreateFrom(0, asset1)); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2))) - .Returns(ResultList.CreateFrom(0, asset2)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); - - var expected = new object[] - { - new - { - data = new - { - findAsset = new - { - id = asset1.Id - } - } - }, - new - { - data = new - { - findAsset = new - { - id = asset2.Id - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_not_return_data_when_field_not_part_of_content() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); - - var query = @" - query { - findMySchemaContent(id: """") { - id - version - created - createdBy - lastModified - lastModifiedBy - url - data { - myInvalid { - iv - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var json = serializer.Serialize(result); - - Assert.Contains("\"data\":null", json); - } - - [Fact] - public async Task Should_return_draft_content_when_querying_dataDraft() - { - var dataDraft = new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "draft value")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 42)); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null, dataDraft); - - var query = @" - query { - findMySchemaContent(id: """") { - dataDraft { - myString { - de - } - myNumber { - iv - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - dataDraft = new - { - myString = new - { - de = "draft value" - }, - myNumber = new - { - iv = 42 - } - } - } - } - }; - - AssertResult(expected, result); - } - - [Fact] - public async Task Should_return_null_when_querying_dataDraft_and_no_draft_content_is_available() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null); - - var query = @" - query { - findMySchemaContent(id: """") { - dataDraft { - myString { - de - } - } - } - }".Replace("", contentId.ToString()); - - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) - .Returns(ResultList.CreateFrom(1, content)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - dataDraft = (object)null - } - } - }; - - AssertResult(expected, result); - } - - private static IReadOnlyList MatchId(Guid contentId) - { - return A>.That.Matches(x => x.Count == 1 && x[0] == contentId); - } - - private static Q MatchIdQuery(Guid contentId) - { - return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); - } - - private Context MatchsAssetContext() - { - return A.That.Matches(x => x.App == app && x.User == requestContext.User); - } - - private Context MatchsContentContext() - { - return A.That.Matches(x => x.App == app && x.User == requestContext.User); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs deleted file mode 100644 index e5eb10df8..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using FakeItEasy; -using GraphQL; -using GraphQL.DataLoader; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using NodaTime; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.TestData; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Log; -using Xunit; - -#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL -{ - public class GraphQLTestBase - { - protected readonly IAppEntity app; - protected readonly IAssetQueryService assetQuery = A.Fake(); - protected readonly IContentQueryService contentQuery = A.Fake(); - protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); - protected readonly ISchemaEntity schema; - protected readonly ISchemaEntity schemaRef1; - protected readonly ISchemaEntity schemaRef2; - protected readonly Context requestContext; - protected readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - protected readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - protected readonly NamedId schemaRefId1 = NamedId.Of(Guid.NewGuid(), "my-ref-schema1"); - protected readonly NamedId schemaRefId2 = NamedId.Of(Guid.NewGuid(), "my-ref-schema2"); - protected readonly IGraphQLService sut; - - public GraphQLTestBase() - { - app = Mocks.App(appId, Language.DE, Language.GermanGermany); - - var schemaDef = - new Schema(schemaId.Name) - .Publish() - .AddJson(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddString(2, "my-string", Partitioning.Language, - new StringFieldProperties()) - .AddNumber(3, "my-number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddNumber(4, "my_number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddAssets(5, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddBoolean(6, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddDateTime(7, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties()) - .AddReferences(8, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaRefId1.Id }) - .AddReferences(81, "my-union", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddReferences(9, "my-invalid", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }) - .AddGeolocation(10, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties()) - .AddTags(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties()) - .AddString(12, "my-localized", Partitioning.Language, - new StringFieldProperties()) - .AddArray(13, "my-array", Partitioning.Invariant, f => f - .AddBoolean(121, "nested-boolean") - .AddNumber(122, "nested-number") - .AddNumber(123, "nested_number")) - .ConfigureScripts(new SchemaScripts { Query = "" }); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - - var schemaRef1Def = - new Schema(schemaRefId1.Name) - .Publish() - .AddString(1, "ref1-field", Partitioning.Invariant); - - schemaRef1 = Mocks.Schema(appId, schemaRefId1, schemaRef1Def); - - var schemaRef2Def = - new Schema(schemaRefId2.Name) - .Publish() - .AddString(1, "ref2-field", Partitioning.Invariant); - - schemaRef2 = Mocks.Schema(appId, schemaRefId2, schemaRef2Def); - - requestContext = new Context(Mocks.FrontendUser(), app); - - sut = CreateSut(); - } - - protected IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - data = data ?? - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "value")) - .AddField("my-assets", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(assetId.ToString()))) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 1.0)) - .AddField("my_number", - new ContentFieldData() - .AddValue("iv", 2.0)) - .AddField("my-boolean", - new ContentFieldData() - .AddValue("iv", true)) - .AddField("my-datetime", - new ContentFieldData() - .AddValue("iv", now)) - .AddField("my-tags", - new ContentFieldData() - .AddValue("iv", JsonValue.Array("tag1", "tag2"))) - .AddField("my-references", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(refId.ToString()))) - .AddField("my-union", - new ContentFieldData() - .AddValue("iv", JsonValue.Array(refId.ToString()))) - .AddField("my-geolocation", - new ContentFieldData() - .AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20))) - .AddField("my-json", - new ContentFieldData() - .AddValue("iv", JsonValue.Object().Add("value", 1))) - .AddField("my-localized", - new ContentFieldData() - .AddValue("de-DE", "de-DE")) - .AddField("my-array", - new ContentFieldData() - .AddValue("iv", JsonValue.Array( - JsonValue.Object() - .Add("nested-boolean", true) - .Add("nested-number", 10) - .Add("nested_number", 11), - JsonValue.Object() - .Add("nested-boolean", false) - .Add("nested-number", 20) - .Add("nested_number", 21)))); - - var content = new ContentEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken(RefTokenType.Subject, "user1"), - LastModified = now, - LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), - Data = data, - DataDraft = dataDraft, - SchemaId = schemaId, - Status = Status.Draft, - StatusColor = "red" - }; - - return content; - } - - protected static IEnrichedContentEntity CreateRefContent(NamedId schemaId, Guid id, string field, string value) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - var data = - new NamedContentData() - .AddField(field, - new ContentFieldData() - .AddValue("iv", value)); - - var content = new ContentEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken(RefTokenType.Subject, "user1"), - LastModified = now, - LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), - Data = data, - DataDraft = data, - SchemaId = schemaId, - Status = Status.Draft, - StatusColor = "red" - }; - - return content; - } - - protected static IEnrichedAssetEntity CreateAsset(Guid id) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - var asset = new AssetEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken(RefTokenType.Subject, "user1"), - LastModified = now, - LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), - FileName = "MyFile.png", - Slug = "myfile.png", - FileSize = 1024, - FileHash = "ABC123", - FileVersion = 123, - MimeType = "image/png", - IsImage = true, - PixelWidth = 800, - PixelHeight = 600, - TagNames = new[] { "tag1", "tag2" }.ToHashSet() - }; - - return asset; - } - - protected void AssertResult(object expected, (bool HasErrors, object Response) result, bool checkErrors = true) - { - if (checkErrors && result.HasErrors) - { - throw new InvalidOperationException(Serialize(result)); - } - - var resultJson = serializer.Serialize(result.Response, true); - var expectJson = serializer.Serialize(expected, true); - - Assert.Equal(expectJson, resultJson); - } - - private string Serialize((bool HasErrors, object Response) result) - { - return serializer.Serialize(result); - } - - private CachingGraphQLService CreateSut() - { - var appProvider = A.Fake(); - - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) - .Returns(new List { schema, schemaRef1, schemaRef2 }); - - var dataLoaderContext = new DataLoaderContextAccessor(); - - var services = new Dictionary - { - [typeof(IAppProvider)] = appProvider, - [typeof(IAssetQueryService)] = assetQuery, - [typeof(IContentQueryService)] = contentQuery, - [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, - [typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(), - [typeof(IOptions)] = Options.Create(new AssetOptions()), - [typeof(IOptions)] = Options.Create(new ContentOptions()), - [typeof(ISemanticLog)] = A.Fake(), - [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) - }; - - var resolver = new FuncDependencyResolver(t => services[t]); - - var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - - return new CachingGraphQLService(cache, resolver); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs deleted file mode 100644 index 76b23f2eb..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ /dev/null @@ -1,289 +0,0 @@ -// ========================================================================== -// 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 FakeItEasy; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using NodaTime.Text; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.MongoDb.Contents; -using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; -using Xunit; -using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; -using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; - -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb -{ - public class MongoDbQueryTests - { - private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry; - private static readonly IBsonSerializer Serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - private readonly Schema schemaDef; - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - - static MongoDbQueryTests() - { - InstantSerializer.Register(); - } - - public MongoDbQueryTests() - { - schemaDef = - new Schema("user") - .AddString(1, "firstName", Partitioning.Language, - new StringFieldProperties()) - .AddString(2, "lastName", Partitioning.Language, - new StringFieldProperties()) - .AddBoolean(3, "isAdmin", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddNumber(4, "age", Partitioning.Invariant, - new NumberFieldProperties()) - .AddDateTime(5, "birthday", Partitioning.Invariant, - new DateTimeFieldProperties()) - .AddAssets(6, "pictures", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddReferences(7, "friends", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddString(8, "dashed-field", Partitioning.Invariant, - new StringFieldProperties()) - .AddArray(9, "hobbies", Partitioning.Invariant, a => a - .AddString(91, "name")) - .Update(new SchemaProperties()); - - var schema = A.Dummy(); - A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); - A.CallTo(() => schema.Version).Returns(3); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - - var app = A.Dummy(); - A.CallTo(() => app.Id).Returns(Guid.NewGuid()); - A.CallTo(() => app.Version).Returns(3); - A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); - } - - [Fact] - public void Should_throw_exception_for_invalid_field() - { - Assert.Throws(() => F(ClrFilter.Eq("data/invalid/iv", "Me"))); - } - - [Fact] - public void Should_make_query_with_lastModified() - { - var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_lastModifiedBy() - { - var i = F(ClrFilter.Eq("lastModifiedBy", "Me")); - var o = C("{ 'mb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_created() - { - var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_createdBy() - { - var i = F(ClrFilter.Eq("createdBy", "Me")); - var o = C("{ 'cb' : 'Me' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version() - { - var i = F(ClrFilter.Eq("version", 0L)); - var o = C("{ 'vs' : NumberLong(0) }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_version_and_list() - { - var i = F(ClrFilter.In("version", new List { 0L, 2L, 5L })); - var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_from_draft() - { - var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"), true); - var o = C("{ 'dd.8.iv' : 'Value' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_empty_test() - { - var i = F(ClrFilter.Empty("data/firstName/iv"), true); - var o = C("{ '$or' : [{ 'dd.1.iv' : { '$exists' : false } }, { 'dd.1.iv' : null }, { 'dd.1.iv' : '' }, { 'dd.1.iv' : [] }] }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_datetime_data() - { - var i = F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); - var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_underscore_field() - { - var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value")); - var o = C("{ 'do.8.iv' : 'Value' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_references_equals() - { - var i = F(ClrFilter.Eq("data/friends/iv", "guid")); - var o = C("{ 'do.7.iv' : 'guid' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_array_field() - { - var i = F(ClrFilter.Eq("data/hobbies/iv/name", "PC")); - var o = C("{ 'do.9.iv.91' : 'PC' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_assets_equals() - { - var i = F(ClrFilter.Eq("data/pictures/iv", "guid")); - var o = C("{ 'do.6.iv' : 'guid' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_query_with_full_text() - { - var i = Q(new ClrQuery { FullText = "Hello my World" }); - var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = S(SortBuilder.Descending("data/age/iv")); - var o = C("{ 'do.4.iv' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en")); - var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_take_statement() - { - var query = new ClrQuery { Take = 3 }; - var cursor = A.Fake>(); - - cursor.ContentTake(query.AdjustToModel(schemaDef, false)); - - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); - } - - [Fact] - public void Should_make_skip_statement() - { - var query = new ClrQuery { Skip = 3 }; - var cursor = A.Fake>(); - - cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); - - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } - - private static string C(string value) - { - return value.Replace('\'', '"'); - } - - private string F(FilterNode filter, bool useDraft = false) - { - return Q(new ClrQuery { Filter = filter }, useDraft); - } - - private string S(params SortNode[] sorts) - { - var cursor = A.Fake>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>.Ignored)) - .Invokes((SortDefinition sortDefinition) => - { - i = sortDefinition.Render(Serializer, Registry).ToString(); - }); - - cursor.ContentSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); - - return i; - } - - private string Q(ClrQuery query, bool useDraft = false) - { - var rendered = - query.AdjustToModel(schemaDef, useDraft).BuildFilter().Filter - .Render(Serializer, Registry).ToString(); - - return rendered; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs deleted file mode 100644 index b1cae86c6..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentEnricherTests - { - private readonly IContentWorkflow contentWorkflow = A.Fake(); - private readonly IContentQueryService contentQuery = A.Fake(); - private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); - private readonly ISchemaEntity schema; - private readonly Context requestContext; - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly ContentEnricher sut; - - public ContentEnricherTests() - { - requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); - - schema = Mocks.Schema(appId, schemaId); - - A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A.Ignored, schemaId.Id.ToString())) - .Returns(schema); - - sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); - } - - [Fact] - public async Task Should_add_app_version_and_schema_as_dependency() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Contains(requestContext.App.Version, result.CacheDependencies); - - Assert.Contains(schema.Id, result.CacheDependencies); - Assert.Contains(schema.Version, result.CacheDependencies); - } - - [Fact] - public async Task Should_enrich_with_reference_fields() - { - var ctx = new Context(Mocks.FrontendUser(), requestContext.App); - - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, ctx); - - Assert.NotNull(result.ReferenceFields); - } - - [Fact] - public async Task Should_not_enrich_with_reference_fields_when_not_frontend() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Null(result.ReferenceFields); - } - - [Fact] - public async Task Should_enrich_with_schema_names() - { - var ctx = new Context(Mocks.FrontendUser(), requestContext.App); - - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, ctx); - - Assert.Equal("my-schema", result.SchemaName); - Assert.Equal("my-schema", result.SchemaDisplayName); - } - - [Fact] - public async Task Should_not_enrich_with_schema_names_when_not_frontend() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Null(result.SchemaName); - Assert.Null(result.SchemaDisplayName); - } - - [Fact] - public async Task Should_enrich_content_with_status_color() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Published, result.StatusColor); - } - - [Fact] - public async Task Should_enrich_content_with_default_color_if_not_found() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(Task.FromResult(null)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Draft, result.StatusColor); - } - - [Fact] - public async Task Should_enrich_content_with_can_update() - { - requestContext.WithResolveFlow(true); - - var source = new ContentEntity { SchemaId = schemaId }; - - A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) - .Returns(true); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.True(result.CanUpdate); - } - - [Fact] - public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() - { - requestContext.WithResolveFlow(false); - - var source = new ContentEntity { SchemaId = schemaId }; - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.False(result.CanUpdate); - - A.CallTo(() => contentWorkflow.CanUpdateAsync(source)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_enrich_multiple_contents_and_cache_color() - { - var source1 = PublishedContent(); - var source2 = PublishedContent(); - - var source = new IContentEntity[] - { - source1, - source2 - }; - - A.CallTo(() => contentWorkflow.GetInfoAsync(source1)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Published, result[0].StatusColor); - Assert.Equal(StatusColors.Published, result[1].StatusColor); - - A.CallTo(() => contentWorkflow.GetInfoAsync(A.Ignored)) - .MustHaveHappenedOnceExactly(); - } - - private ContentEntity PublishedContent() - { - return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs deleted file mode 100644 index 0f9c591a6..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentLoaderTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IContentGrain grain = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly ContentLoader sut; - - public ContentLoaderTests() - { - A.CallTo(() => grainFactory.GetGrain(id, null)) - .Returns(grain); - - sut = new ContentLoader(grainFactory); - } - - [Fact] - public async Task Should_throw_exception_if_no_state_returned() - { - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(null)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_throw_exception_if_state_has_other_version() - { - var content = new ContentEntity { Version = 5 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); - } - - [Fact] - public async Task Should_not_throw_exception_if_state_has_other_version_than_any() - { - var content = new ContentEntity { Version = 5 }; - - A.CallTo(() => grain.GetStateAsync(EtagVersion.Any)) - .Returns(J.Of(content)); - - await sut.GetAsync(id, EtagVersion.Any); - } - - [Fact] - public async Task Should_return_content_from_state() - { - var content = new ContentEntity { Version = 10 }; - - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(J.Of(content)); - - var result = await sut.GetAsync(id, 10); - - Assert.Same(content, result); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs deleted file mode 100644 index bc89098d3..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ /dev/null @@ -1,503 +0,0 @@ -// ========================================================================== -// 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 System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class ContentQueryServiceTests - { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAssetUrlGenerator urlGenerator = A.Fake(); - private readonly IContentEnricher contentEnricher = A.Fake(); - private readonly IContentRepository contentRepository = A.Fake(); - private readonly IContentLoader contentVersionLoader = A.Fake(); - private readonly ISchemaEntity schema; - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly Guid contentId = Guid.NewGuid(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly NamedContentData contentData = new NamedContentData(); - private readonly NamedContentData contentTransformed = new NamedContentData(); - private readonly ClaimsPrincipal user; - private readonly ClaimsIdentity identity = new ClaimsIdentity(); - private readonly Context requestContext; - private readonly ContentQueryParser queryParser = A.Fake(); - private readonly ContentQueryService sut; - - public static IEnumerable ApiStatusTests = new[] - { - new object[] { 0, new[] { Status.Published } }, - new object[] { 1, null } - }; - - public ContentQueryServiceTests() - { - user = new ClaimsPrincipal(identity); - - app = Mocks.App(appId); - - requestContext = new Context(user, app); - - var schemaDef = - new Schema(schemaId.Name) - .ConfigureScripts(new SchemaScripts { Query = "" }); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - - SetupEnricher(); - - A.CallTo(() => queryParser.ParseQuery(requestContext, schema, A.Ignored)) - .Returns(new ClrQuery()); - - sut = new ContentQueryService( - appProvider, - urlGenerator, - contentEnricher, - contentRepository, - contentVersionLoader, - scriptEngine, - queryParser); - } - - [Fact] - public async Task Should_return_schema_from_id_if_string_is_guid() - { - SetupSchemaFound(); - - var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_return_schema_from_name_if_string_not_guid() - { - SetupSchemaFound(); - - var result = await sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_throw_404_if_schema_not_found() - { - SetupSchemaNotFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); - } - - [Fact] - public async Task Should_throw_404_if_schema_not_found_in_check() - { - SetupSchemaNotFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); - } - - [Fact] - public async Task Should_throw_for_single_content_if_no_permission() - { - SetupUser(false, false); - SetupSchemaFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.FindContentAsync(ctx, schemaId.Name, contentId)); - } - - [Fact] - public async Task Should_throw_404_for_single_content_if_content_not_found() - { - var status = new[] { Status.Published }; - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupContent(status, null, includeDraft: false); - - var ctx = requestContext; - - await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, schemaId.Name, contentId)); - } - - [Fact] - public async Task Should_return_single_content_for_frontend_without_transform() - { - var content = CreateContent(contentId); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContent(null, content, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); - - Assert.Equal(contentTransformed, result.Data); - Assert.Equal(content.Id, result.Id); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_return_single_content_for_api_with_transform(int unpublished, Status[] status) - { - var content = CreateContent(contentId); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContent(status, content, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); - - Assert.Equal(contentTransformed, result.Data); - Assert.Equal(content.Id, result.Id); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_return_versioned_content_from_repository_and_transform() - { - var content = CreateContent(contentId); - - SetupUser(true); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - - A.CallTo(() => contentVersionLoader.GetAsync(contentId, 10)) - .Returns(content); - - var ctx = requestContext; - - var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId, 10); - - Assert.Equal(contentTransformed, result.Data); - Assert.Equal(content.Id, result.Id); - } - - [Fact] - public async Task Should_throw_for_query_if_no_permission() - { - SetupUser(false, false); - SetupSchemaFound(); - - var ctx = requestContext; - - await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty)); - } - - [Fact] - public async Task Should_query_contents_by_query_for_frontend_without_transform() - { - const int count = 5, total = 200; - - var content = CreateContent(contentId); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContents(null, count, total, content, inDraft: true, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); - - Assert.Equal(contentData, result[0].Data); - Assert.Equal(content.Id, result[0].Id); - - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_query_contents_by_query_for_api_and_transform(int unpublished, Status[] status) - { - const int count = 5, total = 200; - - var content = CreateContent(contentId); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(contentId); - SetupContents(status, count, total, content, inDraft: false, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); - - Assert.Equal(contentData, result[0].Data); - Assert.Equal(contentId, result[0].Id); - - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(count, Times.Exactly); - } - - [Fact] - public async Task Should_query_contents_by_id_for_frontend_and_transform() - { - const int count = 5, total = 200; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(null, total, ids, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_query_contents_by_id_for_api_and_transform(int unpublished, Status[] status) - { - const int count = 5, total = 200; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(status, total, ids, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids)); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - Assert.Equal(total, result.Total); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(count, Times.Exactly); - } - - [Fact] - public async Task Should_query_all_contents_by_id_for_frontend_and_transform() - { - const int count = 5; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: true); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(null, ids, includeDraft: true); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Theory] - [MemberData(nameof(ApiStatusTests))] - public async Task Should_query_all_contents_by_id_for_api_and_transform(int unpublished, Status[] status) - { - const int count = 5; - - var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: false); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(status, ids, unpublished == 1); - - var ctx = requestContext.WithUnpublished(unpublished == 1); - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - - A.CallTo(() => scriptEngine.Transform(A.Ignored, A.Ignored)) - .MustHaveHappened(count, Times.Exactly); - } - - [Fact] - public async Task Should_skip_contents_when_user_has_no_permission() - { - var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList(); - - SetupUser(isFrontend: false, allowSchema: false); - SetupSchemaFound(); - SetupSchemaScripting(ids.ToArray()); - SetupContents(new Status[0], ids, includeDraft: false); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Empty(result); - } - - [Fact] - public async Task Should_not_call_repository_if_no_id_defined() - { - var ids = new List(); - - SetupUser(isFrontend: false, allowSchema: false); - SetupSchemaFound(); - - var ctx = requestContext; - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Empty(result); - - A.CallTo(() => contentRepository.QueryAsync(app, A.Ignored, A>.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private void SetupUser(bool isFrontend, bool allowSchema = true) - { - if (isFrontend) - { - identity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - } - - if (allowSchema) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.ForApp(Permissions.AppContentsRead, app.Name, schema.SchemaDef.Name).Id)); - } - - requestContext.UpdatePermissions(); - } - - private void SetupSchemaScripting(params Guid[] ids) - { - foreach (var id in ids) - { - A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), "")) - .Returns(contentTransformed); - } - } - - private void SetupContents(Status[] status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft) - { - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), inDraft, A.Ignored, includeDraft)) - .Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); - } - - private void SetupContents(Status[] status, int total, List ids, bool includeDraft) - { - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.Is(status), A>.Ignored, includeDraft)) - .Returns(ResultList.Create(total, ids.Select(CreateContent).Shuffle())); - } - - private void SetupContents(Status[] status, List ids, bool includeDraft) - { - A.CallTo(() => contentRepository.QueryAsync(app, A.That.Is(status), A>.Ignored, includeDraft)) - .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); - } - - private void SetupContent(Status[] status, IContentEntity content, bool includeDraft) - { - A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.Is(status), contentId, includeDraft)) - .Returns(content); - } - - private void SetupSchemaFound() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) - .Returns(schema); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(schema); - } - - private void SetupSchemaNotFound() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) - .Returns((ISchemaEntity)null); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns((ISchemaEntity)null); - } - - private void SetupEnricher() - { - A.CallTo(() => contentEnricher.EnrichAsync(A>.Ignored, requestContext)) - .ReturnsLazily(x => - { - var input = (IEnumerable)x.Arguments[0]; - - return Task.FromResult>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList()); - }); - } - - private IContentEntity CreateContent(Guid id) - { - return CreateContent(id, Status.Published); - } - - private IContentEntity CreateContent(Guid id, Status status) - { - var content = new ContentEntity - { - Id = id, - Data = contentData, - DataDraft = contentData, - SchemaId = schemaId, - Status = status - }; - - return content; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs deleted file mode 100644 index a566fbeec..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using FakeItEasy; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Queries -{ - public class FilterTagTransformerTests - { - private readonly ITagService tagService = A.Fake(); - private readonly ISchemaEntity schema; - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - - public FilterTagTransformerTests() - { - var schemaDef = - new Schema("schema") - .AddTags(1, "tags1", Partitioning.Invariant) - .AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) - .AddString(3, "string", Partitioning.Invariant); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - } - - [Fact] - public void Should_normalize_tags() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A>.That.Contains("name1"))) - .Returns(new Dictionary { ["name1"] = "id1" }); - - var source = ClrFilter.Eq("data.tags2.iv", "name1"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.tags2.iv == 'id1'", result.ToString()); - } - - [Fact] - public void Should_not_fail_when_tags_not_found() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"))) - .Returns(new Dictionary()); - - var source = ClrFilter.Eq("data.tags2.iv", "name1"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.tags2.iv == 'name1'", result.ToString()); - } - - [Fact] - public void Should_not_normalize_other_tags_field() - { - var source = ClrFilter.Eq("data.tags1.iv", "value"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.tags1.iv == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public void Should_not_normalize_other_typed_field() - { - var source = ClrFilter.Eq("data.string.iv", "value"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("data.string.iv == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public void Should_not_normalize_non_data_field() - { - var source = ClrFilter.Eq("no.data", "value"); - - var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); - - Assert.Equal("no.data == 'value'", result.ToString()); - - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs deleted file mode 100644 index ef46b89e5..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerGrainTests.cs +++ /dev/null @@ -1,263 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public class TextIndexerGrainTests : IDisposable - { - private readonly Guid schemaId = Guid.NewGuid(); - private readonly List ids1 = new List { Guid.NewGuid() }; - private readonly List ids2 = new List { Guid.NewGuid() }; - private readonly SearchContext context; - private readonly IAssetStore assetStore = new MemoryAssetStore(); - private readonly TextIndexerGrain sut; - - public TextIndexerGrainTests() - { - context = new SearchContext - { - Languages = new HashSet { "de", "en" } - }; - - sut = new TextIndexerGrain(assetStore); - sut.ActivateAsync(schemaId).Wait(); - } - - public void Dispose() - { - sut.OnDeactivateAsync().Wait(); - } - - [Fact] - public async Task Should_throw_exception_for_invalid_query() - { - await Assert.ThrowsAsync(() => sut.SearchAsync("~hello", context)); - } - - [Fact] - public async Task Should_read_index_and_retrieve() - { - await AddInvariantContent("Hello", "World", false); - - await sut.DeactivateAsync(true); - - var other = new TextIndexerGrain(assetStore); - try - { - await other.ActivateAsync(schemaId); - - await TestSearchAsync(ids1, "Hello", grain: other); - await TestSearchAsync(ids2, "World", grain: other); - } - finally - { - await other.OnDeactivateAsync(); - } - } - - [Fact] - public async Task Should_index_invariant_content_and_retrieve() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "Hello"); - await TestSearchAsync(ids2, "World"); - } - - [Fact] - public async Task Should_index_invariant_content_and_retrieve_with_fuzzy() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "helo~"); - await TestSearchAsync(ids2, "wold~"); - } - - [Fact] - public async Task Should_update_draft_only() - { - await AddInvariantContent("Hello", "World", false); - await AddInvariantContent("Hallo", "Welt", false); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(null, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Hallo", Scope.Draft); - await TestSearchAsync(null, "Hallo", Scope.Published); - } - - [Fact] - public async Task Should_also_update_published_after_copy() - { - await AddInvariantContent("Hello", "World", false); - - await CopyAsync(true); - - await AddInvariantContent("Hallo", "Welt", false); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(null, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Hallo", Scope.Draft); - await TestSearchAsync(ids1, "Hallo", Scope.Published); - } - - [Fact] - public async Task Should_simulate_content_reversion() - { - await AddInvariantContent("Hello", "World", false); - - await CopyAsync(true); - - await AddInvariantContent("Hallo", "Welt", true); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Hallo", Scope.Draft); - await TestSearchAsync(null, "Hallo", Scope.Published); - - await CopyAsync(false); - - await TestSearchAsync(ids1, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - - await TestSearchAsync(null, "Hallo", Scope.Draft); - await TestSearchAsync(null, "Hallo", Scope.Published); - - await AddInvariantContent("Guten Morgen", "Welt", true); - - await TestSearchAsync(null, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - - await TestSearchAsync(ids1, "Guten Morgen", Scope.Draft); - await TestSearchAsync(null, "Guten Morgen", Scope.Published); - } - - [Fact] - public async Task Should_also_retrieve_published_content_after_copy() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "Hello", Scope.Draft); - await TestSearchAsync(null, "Hello", Scope.Published); - - await CopyAsync(true); - - await TestSearchAsync(ids1, "Hello", Scope.Draft); - await TestSearchAsync(ids1, "Hello", Scope.Published); - } - - [Fact] - public async Task Should_delete_documents_from_index() - { - await AddInvariantContent("Hello", "World", false); - - await TestSearchAsync(ids1, "Hello"); - await TestSearchAsync(ids2, "World"); - - await DeleteAsync(ids1[0]); - - await TestSearchAsync(null, "Hello"); - await TestSearchAsync(ids2, "World"); - } - - [Fact] - public async Task Should_search_by_field() - { - await AddLocalizedContent(); - - await TestSearchAsync(null, "de:city"); - await TestSearchAsync(null, "en:Stadt"); - } - - [Fact] - public async Task Should_index_localized_content_and_retrieve() - { - await AddLocalizedContent(); - - await TestSearchAsync(ids1, "Stadt"); - await TestSearchAsync(ids1, "and"); - await TestSearchAsync(ids2, "und"); - - await TestSearchAsync(ids2, "City"); - await TestSearchAsync(ids2, "und"); - await TestSearchAsync(ids1, "and"); - } - - private async Task AddLocalizedContent() - { - var germanData = - new NamedContentData() - .AddField("localized", - new ContentFieldData() - .AddValue("de", "Stadt und Umgebung and whatever")); - - var englishData = - new NamedContentData() - .AddField("localized", - new ContentFieldData() - .AddValue("en", "City and Surroundings und sonstiges")); - - await sut.IndexAsync(new Update { Id = ids1[0], Data = germanData, OnlyDraft = true }); - await sut.IndexAsync(new Update { Id = ids2[0], Data = englishData, OnlyDraft = true }); - } - - private async Task AddInvariantContent(string text1, string text2, bool onlyDraft = false) - { - var data1 = - new NamedContentData() - .AddField("test", - new ContentFieldData() - .AddValue("iv", text1)); - - var data2 = - new NamedContentData() - .AddField("test", - new ContentFieldData() - .AddValue("iv", text2)); - - await sut.IndexAsync(new Update { Id = ids1[0], Data = data1, OnlyDraft = onlyDraft }); - await sut.IndexAsync(new Update { Id = ids2[0], Data = data2, OnlyDraft = onlyDraft }); - } - - private async Task DeleteAsync(Guid id) - { - await sut.DeleteAsync(id); - } - - private async Task CopyAsync(bool fromDraft) - { - await sut.CopyAsync(ids1[0], fromDraft); - await sut.CopyAsync(ids2[0], fromDraft); - } - - private async Task TestSearchAsync(List expected, string text, Scope target = Scope.Draft, TextIndexerGrain grain = null) - { - context.Scope = target; - - var result = await (grain ?? sut).SearchAsync(text, context); - - if (expected != null) - { - Assert.Equal(expected, result); - } - else - { - Assert.Empty(result); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs deleted file mode 100644 index e0bf381d2..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/History/Notifications/NotificationEmailEventConsumerTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.History.Notifications -{ - public class NotificationEmailEventConsumerTests - { - private readonly INotificationEmailSender emailSender = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly IUser assigner = A.Fake(); - private readonly IUser assignee = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly string assignerId = Guid.NewGuid().ToString(); - private readonly string assigneeId = Guid.NewGuid().ToString(); - private readonly string appName = "my-app"; - private readonly NotificationEmailEventConsumer sut; - - public NotificationEmailEventConsumerTests() - { - A.CallTo(() => emailSender.IsActive) - .Returns(true); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) - .Returns(assigner); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) - .Returns(assignee); - - sut = new NotificationEmailEventConsumer(emailSender, userResolver, log); - } - - [Fact] - public async Task Should_not_send_email_if_contributors_assigned_by_clients() - { - var @event = CreateEvent(RefTokenType.Client, true); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_initial_owner() - { - var @event = CreateEvent(RefTokenType.Subject, false, streamNumber: 1); - - await sut.On(@event); - - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_old_events() - { - var @event = CreateEvent(RefTokenType.Subject, true, instant: SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50))); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_for_old_contributor() - { - var @event = CreateEvent(RefTokenType.Subject, true, isNewContributor: false); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_if_sender_not_active() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => emailSender.IsActive) - .Returns(false); - - await sut.On(@event); - - MustNotResolveUser(); - MustNotSendEmail(); - } - - [Fact] - public async Task Should_not_send_email_if_assigner_not_found() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - MustNotSendEmail(); - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_email_if_assignee_not_found() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - MustNotSendEmail(); - MustLogWarning(); - } - - [Fact] - public async Task Should_send_email_for_new_user() - { - var @event = CreateEvent(RefTokenType.Subject, true); - - await sut.On(@event); - - A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, true)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_send_email_for_existing_user() - { - var @event = CreateEvent(RefTokenType.Subject, false); - - await sut.On(@event); - - A.CallTo(() => emailSender.SendContributorEmailAsync(assigner, assignee, appName, false)) - .MustHaveHappened(); - } - - private void MustLogWarning() - { - A.CallTo(() => log.Log(SemanticLogLevel.Warning, A.Ignored, A>.Ignored)) - .MustHaveHappened(); - } - - private void MustNotResolveUser() - { - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - private void MustNotSendEmail() - { - A.CallTo(() => emailSender.SendContributorEmailAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private Envelope CreateEvent(string assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) - { - var @event = new AppContributorAssigned - { - Actor = new RefToken(assignerType, assignerId), - AppId = NamedId.Of(Guid.NewGuid(), appName), - ContributorId = assigneeId, - IsCreated = isNewUser, - IsAdded = isNewContributor - }; - - var envelope = Envelope.Create(@event); - - envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); - envelope.SetEventStreamNumber(streamNumber); - - return envelope; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs deleted file mode 100644 index 263b27a7a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Rules.Guards -{ - public class GuardRuleTests - { - private readonly Uri validUrl = new Uri("https://squidex.io"); - private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction()).Rename("MyName"); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly IAppProvider appProvider = A.Fake(); - - public sealed class TestAction : RuleAction - { - public Uri Url { get; set; } - } - - public GuardRuleTests() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(Mocks.Schema(appId, schemaId)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_trigger_null() - { - var command = CreateCommand(new CreateRule - { - Trigger = null, - Action = new TestAction - { - Url = validUrl - } - }); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), - new ValidationError("Trigger is required.", "Trigger")); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_action_null() - { - var command = CreateCommand(new CreateRule - { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }, - Action = null - }); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), - new ValidationError("Action is required.", "Action")); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() - { - var command = CreateCommand(new CreateRule - { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }, - Action = new TestAction - { - Url = validUrl - } - }); - - await GuardRule.CanCreate(command, appProvider); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() - { - var command = new UpdateRule(); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), - new ValidationError("Either trigger, action or name is required.", "Trigger", "Action")); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_rule_has_already_this_name() - { - var command = new UpdateRule - { - Name = "MyName" - }; - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0), - new ValidationError("Rule has already this name.", "Name")); - } - - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_trigger_action__and_name_are_valid() - { - var command = new UpdateRule - { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }, - Action = new TestAction - { - Url = validUrl - }, - Name = "NewName" - }; - - await GuardRule.CanUpdate(command, appId.Id, appProvider, rule_0); - } - - [Fact] - public void CanEnable_should_throw_exception_if_rule_enabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Enable(); - - Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); - } - - [Fact] - public void CanEnable_should_not_throw_exception_if_rule_disabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Disable(); - - GuardRule.CanEnable(command, rule_1); - } - - [Fact] - public void CanDisable_should_throw_exception_if_rule_disabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Disable(); - - Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); - } - - [Fact] - public void CanDisable_should_not_throw_exception_if_rule_enabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Enable(); - - GuardRule.CanDisable(command, rule_1); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteRule(); - - GuardRule.CanDelete(command); - } - - private CreateRule CreateCommand(CreateRule command) - { - command.AppId = appId; - - return command; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs deleted file mode 100644 index e871bf736..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers -{ - public class ContentChangedTriggerTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - - [Fact] - public async Task Should_add_error_if_schema_id_is_not_defined() - { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2()) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Schema id is required.", "Schemas") - }); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_error_if_schemas_ids_are_not_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(Task.FromResult(null)); - - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError($"Schema {schemaId.Id} does not exist.", "Schemas") - }); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_is_null() - { - var trigger = new ContentChangedTriggerV2(); - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_is_empty() - { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_ids_are_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) - .Returns(Mocks.Schema(appId, schemaId)); - - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs deleted file mode 100644 index 569ca61f7..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class ManualTriggerHandlerTests - { - private readonly IRuleTriggerHandler sut = new ManualTriggerHandler(); - - [Fact] - public async Task Should_create_event_with_name() - { - var envelope = Envelope.Create(new RuleManuallyTriggered()); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal("Manual", result.Name); - } - - [Fact] - public void Should_always_trigger() - { - Assert.True(sut.Trigger(new EnrichedManualEvent(), new ManualTrigger())); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs deleted file mode 100644 index b9455635c..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class RuleEnqueuerTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly RuleService ruleService = A.Fake(); - private readonly RuleEnqueuer sut; - - public sealed class TestAction : RuleAction - { - public Uri Url { get; set; } - } - - public RuleEnqueuerTests() - { - sut = new RuleEnqueuer( - appProvider, - cache, - ruleEventRepository, - ruleService); - } - - [Fact] - public void Should_return_contents_filter_for_events_filter() - { - Assert.Equal(".*", sut.EventsFilter); - } - - [Fact] - public void Should_return_type_name_for_name() - { - Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); - } - - [Fact] - public async Task Should_do_nothing_on_clear() - { - await sut.ClearAsync(); - } - - [Fact] - public async Task Should_update_repository_when_enqueing() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId }); - - var rule = CreateRule(); - - var job = new RuleJob { Created = now }; - - A.CallTo(() => ruleService.CreateJobAsync(rule.RuleDef, rule.Id, @event)) - .Returns(job); - - await sut.Enqueue(rule.RuleDef, rule.Id, @event); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_update_repositories_with_jobs_from_service() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId }); - - var rule1 = CreateRule(); - var rule2 = CreateRule(); - - var job1 = new RuleJob { Created = now }; - - A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) - .Returns(new List { rule1, rule2 }); - - A.CallTo(() => ruleService.CreateJobAsync(rule1.RuleDef, rule1.Id, @event)) - .Returns(job1); - - A.CallTo(() => ruleService.CreateJobAsync(rule2.RuleDef, rule2.Id, @event)) - .Returns(Task.FromResult(null)); - - await sut.On(@event); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) - .MustHaveHappened(); - } - - private static RuleEntity CreateRule() - { - var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); - - return new RuleEntity { RuleDef = rule, Id = Guid.NewGuid() }; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs deleted file mode 100644 index b8ca9421b..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public class UsageTriggerHandlerTests - { - private readonly Guid ruleId = Guid.NewGuid(); - private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - var result = sut.Trigger(new ContentCreated(), new UsageTrigger(), ruleId); - - Assert.False(result); - } - - [Fact] - public void Should_not_trigger_precheck_when_rule_id_not_matchs() - { - var result = sut.Trigger(new AppUsageExceeded { RuleId = Guid.NewGuid() }, new UsageTrigger(), ruleId); - - Assert.True(result); - } - - [Fact] - public void Should_trigger_precheck_when_event_type_correct_and_rule_id_matchs() - { - var result = sut.Trigger(new AppUsageExceeded { RuleId = ruleId }, new UsageTrigger(), ruleId); - - Assert.True(result); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); - - Assert.False(result); - } - - [Fact] - public async Task Should_create_enriched_event() - { - var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; - - var result = (EnrichedUsageExceededEvent)await sut.CreateEnrichedEventAsync(Envelope.Create(@event)); - - Assert.Equal(@event.CallsCurrent, result.CallsCurrent); - Assert.Equal(@event.CallsLimit, result.CallsLimit); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs deleted file mode 100644 index 5320d862d..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs +++ /dev/null @@ -1,379 +0,0 @@ -// ========================================================================== -// 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.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public class GuardSchemaFieldTests - { - private readonly Schema schema_0; - private readonly StringFieldProperties validProperties = new StringFieldProperties(); - private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - - public GuardSchemaFieldTests() - { - schema_0 = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant) - .AddString(2, "field2", Partitioning.Invariant) - .AddArray(3, "field3", Partitioning.Invariant, f => f - .AddNumber(301, "field301")) - .AddUI(4, "field4", Partitioning.Invariant); - } - - private static Action A(Action method) where T : FieldCommand - { - return method; - } - - private static Func S(Func method) - { - return method; - } - - public static IEnumerable FieldCommandData = new[] - { - new object[] { A(GuardSchemaField.CanEnable) }, - new object[] { A(GuardSchemaField.CanDelete) }, - new object[] { A(GuardSchemaField.CanDisable) }, - new object[] { A(GuardSchemaField.CanHide) }, - new object[] { A(GuardSchemaField.CanLock) }, - new object[] { A(GuardSchemaField.CanShow) }, - new object[] { A(GuardSchemaField.CanUpdate) } - }; - - public static IEnumerable InvalidStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(1)) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(1)) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.LockField(1)) }, - new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(1)) } - }; - - public static IEnumerable InvalidNestedStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s.DisableField(301, 3)) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s.HideField(301, 3)) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s) }, - new object[] { A(GuardSchemaField.CanLock), S(s => s.LockField(301, 3)) } - }; - - public static IEnumerable ValidStates = new[] - { - new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, - new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(1)) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(1)) } - }; - - public static IEnumerable ValidNestedStates = new[] - { - new object[] { A(GuardSchemaField.CanEnable), S(s => s.DisableField(301, 3)) }, - new object[] { A(GuardSchemaField.CanDisable), S(s => s) }, - new object[] { A(GuardSchemaField.CanHide), S(s => s) }, - new object[] { A(GuardSchemaField.CanShow), S(s => s.HideField(301, 3)) } - }; - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_field_not_found(Action action) where T : FieldCommand, new() - { - var command = new T { FieldId = 5 }; - - Assert.Throws(() => action(schema_0, command)); - } - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_parent_field_not_found(Action action) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 4, FieldId = 401 }; - - Assert.Throws(() => action(schema_0, command)); - } - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_child_field_not_found(Action action) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 302 }; - - Assert.Throws(() => action(schema_0, command)); - } - - [Theory] - [MemberData(nameof(InvalidStates))] - public void Commands_should_throw_exception_if_state_not_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { FieldId = 1 }; - - Assert.Throws(() => action(updater(schema_0), command)); - } - - [Theory] - [MemberData(nameof(InvalidNestedStates))] - public void Commands_should_throw_exception_if_nested_state_not_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 301 }; - - Assert.Throws(() => action(updater(schema_0), command)); - } - - [Theory] - [MemberData(nameof(ValidStates))] - public void Commands_should_not_throw_exception_if_state_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { FieldId = 1 }; - - action(updater(schema_0), command); - } - - [Theory] - [MemberData(nameof(ValidNestedStates))] - public void Commands_should_not_throw_exception_if_nested_state_valid(Action action, Func updater) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 301 }; - - action(updater(schema_0), command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_locked() - { - var command = new DeleteField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - - Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); - } - - [Fact] - public void CanDisable_should_throw_exception_if_already_disabled() - { - var command = new DisableField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Disable()); - - Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); - } - - [Fact] - public void CanDisable_should_throw_exception_if_ui_field() - { - var command = new DisableField { FieldId = 4 }; - - Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); - } - - [Fact] - public void CanEnable_should_throw_exception_if_already_enabled() - { - var command = new EnableField { FieldId = 1 }; - - Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_locked() - { - var command = new HideField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_already_hidden() - { - var command = new HideField { FieldId = 1 }; - - var schema_1 = schema_0.UpdateField(1, f => f.Hide()); - - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_ui_field() - { - var command = new HideField { FieldId = 4 }; - - Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); - } - - [Fact] - public void CanShow_should_throw_exception_if_already_visible() - { - var command = new ShowField { FieldId = 4 }; - - Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_not_locked() - { - var command = new DeleteField { FieldId = 1 }; - - GuardSchemaField.CanDelete(schema_0, command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_locked() - { - var command = new UpdateField { FieldId = 1, Properties = validProperties }; - - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - - Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_not_locked() - { - var command = new UpdateField { FieldId = 1, Properties = validProperties }; - - GuardSchemaField.CanUpdate(schema_0, command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_list_field() - { - var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsListField = true } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_marking_a_ui_field_as_reference_field() - { - var command = new UpdateField { FieldId = 4, Properties = new UIFieldProperties { IsReferenceField = true } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("UI field cannot be a reference field.", "Properties.IsReferenceField")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_properties_null() - { - var command = new UpdateField { FieldId = 2, Properties = null }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("Properties is required.", "Properties")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_properties_not_valid() - { - var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(schema_0, command), - new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_field_already_exists() - { - var command = new AddField { Name = "field1", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("A field with the same name already exists.")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_nested_field_already_exists() - { - var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("A field with the same name already exists.")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_not_valid() - { - var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Name must be a valid javascript property name.", "Name")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_properties_not_valid() - { - var command = new AddField { Name = "field5", Properties = invalidProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_properties_null() - { - var command = new AddField { Name = "field5", Properties = null }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Properties is required.", "Properties")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_partitioning_not_valid() - { - var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("Partitioning is not a valid value.", "Partitioning")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_creating_a_ui_field_as_list_field() - { - var command = new AddField { Name = "field5", Properties = new UIFieldProperties { IsListField = true } }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(schema_0, command), - new ValidationError("UI field cannot be a list field.", "Properties.IsListField")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_parent_not_exists() - { - var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; - - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_not_exists() - { - var command = new AddField { Name = "field5", Properties = validProperties }; - - GuardSchemaField.CanAdd(schema_0, command); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_exists_in_root() - { - var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; - - GuardSchemaField.CanAdd(schema_0, command); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs deleted file mode 100644 index 557737a10..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ /dev/null @@ -1,530 +0,0 @@ -// ========================================================================== -// 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.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Entities.Schemas.Guards -{ - public class GuardSchemaTests - { - private readonly Schema schema_0; - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - - public GuardSchemaTests() - { - schema_0 = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant) - .AddString(2, "field2", Partitioning.Invariant); - } - - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Name is not a valid slug.", "Name")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_name_invalid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "invalid name", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field name must be a valid javascript property name.", - "Fields[1].Name")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_properties_null() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = null, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field properties is required.", - "Fields[1].Properties")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_properties_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Max length must be greater or equal to min length.", - "Fields[1].Properties.MinLength", - "Fields[1].Properties.MaxLength")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = "INVALID" - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Partitioning is not a valid value.", - "Fields[1].Partitioning")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Fields cannot have duplicate names.", - "Fields")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_name_invalid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "invalid name", - Properties = new StringFieldProperties() - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field name must be a valid javascript property name.", - "Fields[1].Nested[1].Name")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_properties_null() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = null - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field properties is required.", - "Fields[1].Nested[1].Properties")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_is_array() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new ArrayFieldProperties() - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Nested field cannot be array fields.", - "Fields[1].Nested[1].Properties")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Max length must be greater or equal to min length.", - "Fields[1].Nested[1].Properties.MinLength", - "Fields[1].Nested[1].Properties.MaxLength")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties() - }, - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties() - } - } - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Fields cannot have duplicate names.", - "Fields[1].Nested")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_ui_field_is_invalid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new UIFieldProperties - { - IsListField = true, - IsReferenceField = true - }, - IsHidden = true, - IsDisabled = true, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }; - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("UI field cannot be a list field.", - "Fields[1].Properties.IsListField"), - new ValidationError("UI field cannot be a reference field.", - "Fields[1].Properties.IsReferenceField"), - new ValidationError("UI field cannot be hidden.", - "Fields[1].IsHidden"), - new ValidationError("UI field cannot be disabled.", - "Fields[1].IsDisabled")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_command_is_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties - { - IsListField = true - }, - IsHidden = true, - IsDisabled = true, - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field2", - Properties = ValidProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field3", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new List - { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = ValidProperties() - }, - new UpsertSchemaNestedField - { - Name = "nested2", - Properties = ValidProperties() - } - } - } - }, - Name = "new-schema" - }; - - GuardSchema.CanCreate(command); - } - - [Fact] - public void CanPublish_should_throw_exception_if_already_published() - { - var command = new PublishSchema(); - - var schema_1 = schema_0.Publish(); - - Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); - } - - [Fact] - public void CanPublish_should_not_throw_exception_if_not_published() - { - var command = new PublishSchema(); - - GuardSchema.CanPublish(schema_0, command); - } - - [Fact] - public void CanUnpublish_should_throw_exception_if_already_unpublished() - { - var command = new UnpublishSchema(); - - Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); - } - - [Fact] - public void CanUnpublish_should_not_throw_exception_if_already_published() - { - var command = new UnpublishSchema(); - - var schema_1 = schema_0.Publish(); - - GuardSchema.CanUnpublish(schema_1, command); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() - { - var command = new ReorderFields { FieldIds = new List { 1, 3 } }; - - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), - new ValidationError("Field ids do not cover all fields.", "FieldIds")); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() - { - var command = new ReorderFields { FieldIds = new List { 1 } }; - - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), - new ValidationError("Field ids do not cover all fields.", "FieldIds")); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_null() - { - var command = new ReorderFields { FieldIds = null }; - - ValidationAssert.Throws(() => GuardSchema.CanReorder(schema_0, command), - new ValidationError("Field ids is required.", "FieldIds")); - } - - [Fact] - public void CanReorder_should_throw_exception_if_parent_field_not_found() - { - var command = new ReorderFields { FieldIds = new List { 1, 2 }, ParentFieldId = 99 }; - - Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); - } - - [Fact] - public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() - { - var command = new ReorderFields { FieldIds = new List { 1, 2 } }; - - GuardSchema.CanReorder(schema_0, command); - } - - [Fact] - public void CanConfigurePreviewUrls_should_throw_exception_if_preview_urls_null() - { - var command = new ConfigurePreviewUrls { PreviewUrls = null }; - - ValidationAssert.Throws(() => GuardSchema.CanConfigurePreviewUrls(command), - new ValidationError("Preview Urls is required.", "PreviewUrls")); - } - - [Fact] - public void CanConfigurePreviewUrls_should_not_throw_exception_if_valid() - { - var command = new ConfigurePreviewUrls { PreviewUrls = new Dictionary() }; - - GuardSchema.CanConfigurePreviewUrls(command); - } - - [Fact] - public void CanChangeCategory_should_not_throw_exception() - { - var command = new ChangeCategory(); - - GuardSchema.CanChangeCategory(schema_0, command); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteSchema(); - - GuardSchema.CanDelete(schema_0, command); - } - - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs deleted file mode 100644 index 7f73232cf..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ /dev/null @@ -1,248 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public class SchemasIndexTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly ISchemasByAppIndexGrain index = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly SchemasIndex sut; - - public SchemasIndexTests() - { - A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) - .Returns(index); - - sut = new SchemasIndex(grainFactory); - } - - [Fact] - public async Task Should_resolve_schema_by_id() - { - var schema = SetupSchema(0, false); - - var actual = await sut.GetSchemaAsync(appId.Id, schema.Id); - - Assert.Same(actual, schema); - } - - [Fact] - public async Task Should_resolve_schema_by_name() - { - var schema = SetupSchema(0, false); - - A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) - .Returns(schema.Id); - - var actual = await sut.GetSchemaByNameAsync(appId.Id, schema.SchemaDef.Name); - - Assert.Same(actual, schema); - } - - [Fact] - public async Task Should_resolve_schemas_by_id() - { - var schema = SetupSchema(0, false); - - A.CallTo(() => index.GetIdsAsync()) - .Returns(new List { schema.Id }); - - var actual = await sut.GetSchemasAsync(appId.Id); - - Assert.Same(actual[0], schema); - } - - [Fact] - public async Task Should_return_empty_schema_if_schema_not_created() - { - var schema = SetupSchema(-1, false); - - A.CallTo(() => index.GetIdsAsync()) - .Returns(new List { schema.Id }); - - var actual = await sut.GetSchemasAsync(appId.Id); - - Assert.Empty(actual); - } - - [Fact] - public async Task Should_return_empty_schema_if_schema_deleted() - { - var schema = SetupSchema(0, true); - - A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) - .Returns(schema.Id); - - var actual = await sut.GetSchemasAsync(appId.Id); - - Assert.Empty(actual); - } - - [Fact] - public async Task Should_also_return_schema_if_deleted_allowed() - { - var schema = SetupSchema(-1, true); - - A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) - .Returns(schema.Id); - - var actual = await sut.GetSchemasAsync(appId.Id, true); - - Assert.Empty(actual); - } - - [Fact] - public async Task Should_add_schema_to_index_on_create() - { - var token = RandomHash.Simple(); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(token); - - var context = - new CommandContext(Create(schemaId.Name), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_clear_reservation_when_schema_creation_failed() - { - var token = RandomHash.Simple(); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(token); - - var context = - new CommandContext(Create(schemaId.Name), commandBus); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddAsync(token)) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(token)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_index_on_create_if_name_taken() - { - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(Task.FromResult(null)); - - var context = - new CommandContext(Create(schemaId.Name), commandBus) - .Complete(); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => index.AddAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_add_to_index_on_create_if_name_invalid() - { - var context = - new CommandContext(Create("INVALID"), commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_remove_schema_from_index_on_delete() - { - var schema = SetupSchema(0, false); - - var context = - new CommandContext(new DeleteSchema { SchemaId = schema.Id }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveAsync(schema.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_when_rebuilding() - { - var schemas = new Dictionary(); - - await sut.RebuildAsync(appId.Id, schemas); - - A.CallTo(() => index.RebuildAsync(schemas)) - .MustHaveHappened(); - } - - private CreateSchema Create(string name) - { - return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; - } - - private ISchemaEntity SetupSchema(long version, bool deleted) - { - var schemaEntity = A.Fake(); - - A.CallTo(() => schemaEntity.SchemaDef) - .Returns(new Schema(schemaId.Name)); - A.CallTo(() => schemaEntity.Id) - .Returns(schemaId.Id); - A.CallTo(() => schemaEntity.AppId) - .Returns(appId); - A.CallTo(() => schemaEntity.Version) - .Returns(version); - A.CallTo(() => schemaEntity.IsDeleted) - .Returns(deleted); - - var schemaGrain = A.Fake(); - - A.CallTo(() => schemaGrain.GetStateAsync()) - .Returns(J.Of(schemaEntity)); - - A.CallTo(() => grainFactory.GetGrain(schemaId.Id, null)) - .Returns(schemaGrain); - - return schemaEntity; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs deleted file mode 100644 index 2501192a3..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -#pragma warning disable SA1401 // Fields must be private - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public class SchemaChangedTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IRuleTriggerHandler sut; - - public SchemaChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) - .Returns(true); - - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) - .Returns(false); - - sut = new SchemaChangedTriggerHandler(scriptEngine); - } - - public static IEnumerable TestEvents = new[] - { - new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }, - new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }, - new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }, - new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }, - new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished } - }; - - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_enrich_events(SchemaEvent @event, EnrichedSchemaEventType type) - { - var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - - var result = await sut.CreateEnrichedEventAsync(envelope); - - Assert.Equal(type, ((EnrichedSchemaEvent)result).Type); - } - - [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new AppCreated(), trigger, Guid.NewGuid()); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_precheck_when_event_type_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new SchemaCreated(), trigger, Guid.NewGuid()); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedContentEvent(), trigger); - - Assert.False(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_is_empty() - { - TestForCondition(string.Empty, trigger => - { - var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_trigger_check_when_condition_matchs() - { - TestForCondition("true", trigger => - { - var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); - - Assert.True(result); - }); - } - - [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() - { - TestForCondition("false", trigger => - { - var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); - - Assert.False(result); - }); - } - - private void TestForCondition(string condition, Action action) - { - var trigger = new SchemaChangedTrigger { Condition = condition }; - - action(trigger); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) - .MustHaveHappened(); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj deleted file mode 100644 index 3171989f0..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Domain.Apps.Entities - 7.3 - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs deleted file mode 100644 index 5ec9b5fce..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.TestHelpers -{ - public static class AExtensions - { - public static ClrQuery Is(this INegatableArgumentConstraintManager that, string query) - { - return that.Matches(x => x.ToString() == query); - } - - public static T[] Is(this INegatableArgumentConstraintManager that, params T[] values) - { - if (values == null) - { - return that.IsNull(); - } - - return that.IsSameSequenceAs(values); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs deleted file mode 100644 index 5980e336e..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/JsonHelper.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.TestHelpers -{ - public static class JsonHelper - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - - public static IJsonSerializer CreateSerializer(TypeNameRegistry typeNameRegistry = null) - { - var serializerSettings = DefaultSettings(typeNameRegistry); - - return new NewtonsoftJsonSerializer(serializerSettings); - } - - public static JsonSerializerSettings DefaultSettings(TypeNameRegistry typeNameRegistry = null) - { - return new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), - - ContractResolver = new ConverterContractResolver( - new ClaimsPrincipalConverter(), - new InstantConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new StringEnumConverter()), - - TypeNameHandling = TypeNameHandling.Auto - }; - } - - public static T SerializeAndDeserialize(this T value) - { - return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; - } - - public static T Deserialize(string value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; - } - - public static T Deserialize(object value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs deleted file mode 100644 index 3aa99dc28..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Entities.TestHelpers -{ - public static class Mocks - { - public static IAppEntity App(NamedId appId, params Language[] languages) - { - var config = LanguagesConfig.English; - - foreach (var language in languages) - { - config = config.Set(language); - } - - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - A.CallTo(() => app.LanguagesConfig).Returns(config); - - return app; - } - - public static ISchemaEntity Schema(NamedId appId, NamedId schemaId, Schema schemaDef = null) - { - var schema = A.Fake(); - - A.CallTo(() => schema.Id).Returns(schemaId.Id); - A.CallTo(() => schema.AppId).Returns(appId); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef ?? new Schema(schemaId.Name)); - - return schema; - } - - public static ClaimsPrincipal ApiUser(string role = null) - { - return CreateUser(role, "api"); - } - - public static ClaimsPrincipal FrontendUser(string role = null) - { - return CreateUser(role, DefaultClients.Frontend); - } - - private static ClaimsPrincipal CreateUser(string role, string client) - { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); - - if (role != null) - { - claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); - } - - return claimsPrincipal; - } - } -} diff --git a/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs deleted file mode 100644 index 1854b3f52..000000000 --- a/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Identity; -using Xunit; - -namespace Squidex.Domain.Users -{ - public class DefaultUserResolverTests - { - private readonly UserManager userManager = A.Fake>(); - private readonly DefaultUserResolver sut; - - public DefaultUserResolverTests() - { - var userFactory = A.Fake(); - - A.CallTo(() => userFactory.IsId(A.That.StartsWith("id"))) - .Returns(true); - - A.CallTo(() => userManager.NormalizeKey(A.Ignored)) - .ReturnsLazily(c => c.GetArgument(0).ToUpperInvariant()); - - sut = new DefaultUserResolver(userManager, userFactory); - } - - [Fact] - public async Task Should_resolve_user_by_email() - { - var (user, claims) = GernerateUser("id1"); - - A.CallTo(() => userManager.FindByEmailAsync(user.Email)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); - - var result = await sut.FindByIdOrEmailAsync(user.Email); - - Assert.Equal(user.Id, result.Id); - Assert.Equal(user.Email, result.Email); - - Assert.Equal(claims, result.Claims); - } - - [Fact] - public async Task Should_resolve_user_by_id1() - { - var (user, claims) = GernerateUser("id2"); - - A.CallTo(() => userManager.FindByIdAsync(user.Id)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); - - var result = await sut.FindByIdOrEmailAsync(user.Id); - - Assert.Equal(user.Id, result.Id); - Assert.Equal(user.Email, result.Email); - - Assert.Equal(claims, result.Claims); - } - - [Fact] - public async Task Should_query_many_by_email_async() - { - var (user1, claims1) = GernerateUser("id1"); - var (user2, claims2) = GernerateUser("id2"); - - var list = new List { user1, user2 }; - - A.CallTo(() => userManager.Users) - .Returns(list.AsQueryable()); - - A.CallTo(() => userManager.GetClaimsAsync(user2)) - .Returns(claims2); - - var result = await sut.QueryByEmailAsync("2"); - - Assert.Equal(user2.Id, result[0].Id); - Assert.Equal(user2.Email, result[0].Email); - - Assert.Equal(claims2, result[0].Claims); - - A.CallTo(() => userManager.GetClaimsAsync(user1)) - .MustNotHaveHappened(); - } - - private static (IdentityUser, List) GernerateUser(string id) - { - var user = new IdentityUser { Id = id, Email = $"email_{id}", NormalizedEmail = $"EMAIL_{id}" }; - - var claims = new List - { - new Claim($"{id}_a", "1"), - new Claim($"{id}_b", "2") - }; - - return (user, claims); - } - } -} diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj deleted file mode 100644 index 5eddc1a8c..000000000 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Domain.Users - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs deleted file mode 100644 index d27d40792..000000000 --- a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Assets -{ - public abstract class AssetStoreTests where T : IAssetStore - { - private readonly MemoryStream assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); - private readonly string fileName = Guid.NewGuid().ToString(); - private readonly string sourceFile = Guid.NewGuid().ToString(); - private readonly Lazy sut; - - protected T Sut - { - get { return sut.Value; } - } - - protected string FileName - { - get { return fileName; } - } - - protected AssetStoreTests() - { - sut = new Lazy(CreateStore); - } - - public abstract T CreateStore(); - - [Fact] - public virtual async Task Should_throw_exception_if_asset_to_download_is_not_found() - { - await Assert.ThrowsAsync(() => Sut.DownloadAsync(fileName, new MemoryStream())); - } - - [Fact] - public async Task Should_throw_exception_if_asset_to_copy_is_not_found() - { - await Assert.ThrowsAsync(() => Sut.CopyAsync(fileName, sourceFile)); - } - - [Fact] - public async Task Should_throw_exception_if_stream_to_download_is_null() - { - await Assert.ThrowsAsync(() => Sut.DownloadAsync("File", null)); - } - - [Fact] - public async Task Should_throw_exception_if_stream_to_upload_is_null() - { - await Assert.ThrowsAsync(() => Sut.UploadAsync("File", null)); - } - - [Fact] - public async Task Should_throw_exception_if_source_file_name_to_copy_is_empty() - { - await CheckEmpty(v => Sut.CopyAsync(v, "Target")); - } - - [Fact] - public async Task Should_throw_exception_if_target_file_name_to_copy_is_empty() - { - await CheckEmpty(v => Sut.CopyAsync("Source", v)); - } - - [Fact] - public async Task Should_throw_exception_if_file_name_to_delete_is_empty() - { - await CheckEmpty(v => Sut.DeleteAsync(v)); - } - - [Fact] - public async Task Should_throw_exception_if_file_name_to_download_is_empty() - { - await CheckEmpty(v => Sut.DownloadAsync(v, new MemoryStream())); - } - - [Fact] - public async Task Should_throw_exception_if_file_name_to_upload_is_empty() - { - await CheckEmpty(v => Sut.UploadAsync(v, new MemoryStream())); - } - - [Fact] - public async Task Should_write_and_read_file() - { - await Sut.UploadAsync(fileName, assetData); - - var readData = new MemoryStream(); - - await Sut.DownloadAsync(fileName, readData); - - Assert.Equal(assetData.ToArray(), readData.ToArray()); - } - - [Fact] - public async Task Should_write_and_read_file_and_overwrite_non_existing() - { - await Sut.UploadAsync(fileName, assetData, true); - - var readData = new MemoryStream(); - - await Sut.DownloadAsync(fileName, readData); - - Assert.Equal(assetData.ToArray(), readData.ToArray()); - } - - [Fact] - public async Task Should_write_and_read_overriding_file() - { - var oldData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }); - - await Sut.UploadAsync(fileName, oldData); - await Sut.UploadAsync(fileName, assetData, true); - - var readData = new MemoryStream(); - - await Sut.DownloadAsync(fileName, readData); - - Assert.Equal(assetData.ToArray(), readData.ToArray()); - } - - [Fact] - public async Task Should_throw_exception_when_file_to_write_already_exists() - { - await Sut.UploadAsync(fileName, assetData); - - await Assert.ThrowsAsync(() => Sut.UploadAsync(fileName, assetData)); - } - - [Fact] - public async Task Should_throw_exception_when_target_file_to_copy_to_already_exists() - { - await Sut.UploadAsync(sourceFile, assetData); - await Sut.CopyAsync(sourceFile, fileName); - - await Assert.ThrowsAsync(() => Sut.CopyAsync(sourceFile, fileName)); - } - - [Fact] - public async Task Should_ignore_when_deleting_not_existing_file() - { - await Sut.UploadAsync(sourceFile, assetData); - await Sut.DeleteAsync(sourceFile); - await Sut.DeleteAsync(sourceFile); - } - - private static async Task CheckEmpty(Func action) - { - await Assert.ThrowsAsync(() => action(null)); - await Assert.ThrowsAsync(() => action(string.Empty)); - await Assert.ThrowsAsync(() => action(" ")); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs deleted file mode 100644 index f9f75a47d..000000000 --- a/tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using Squidex.Infrastructure.Assets.ImageSharp; -using Xunit; - -namespace Squidex.Infrastructure.Assets -{ - public class ImageSharpAssetThumbnailGeneratorTests - { - private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator(); - private readonly MemoryStream target = new MemoryStream(); - - [Fact] - public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail() - { - var source = GetPng(); - - await sut.CreateThumbnailAsync(source, target); - - Assert.Equal(target.Length, source.Length); - } - - [Fact] - public async Task Should_resize_image_to_target() - { - var source = GetPng(); - - await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize"); - - Assert.True(target.Length > source.Length); - } - - [Fact] - public async Task Should_change_jpeg_quality_and_write_to_target() - { - var source = GetJpeg(); - - await sut.CreateThumbnailAsync(source, target, quality: 10); - - Assert.True(target.Length < source.Length); - } - - [Fact] - public async Task Should_change_png_quality_and_write_to_target() - { - var source = GetPng(); - - await sut.CreateThumbnailAsync(source, target, quality: 10); - - Assert.True(target.Length < source.Length); - } - - [Fact] - public async Task Should_return_image_information_if_image_is_valid() - { - var source = GetPng(); - - var imageInfo = await sut.GetImageInfoAsync(source); - - Assert.Equal(600, imageInfo.PixelHeight); - Assert.Equal(600, imageInfo.PixelWidth); - } - - [Fact] - public async Task Should_return_null_if_stream_is_not_an_image() - { - var source = new MemoryStream(Convert.FromBase64String("YXNkc2Fk")); - - var imageInfo = await sut.GetImageInfoAsync(source); - - Assert.Null(imageInfo); - } - - private Stream GetPng() - { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png"); - } - - private Stream GetJpeg() - { - return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg"); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs deleted file mode 100644 index 8a2851e74..000000000 --- a/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class CollectionExtensionsTests - { - private readonly Dictionary valueDictionary = new Dictionary(); - private readonly Dictionary> listDictionary = new Dictionary>(); - - [Fact] - public void GetOrDefault_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrDefault(12)); - } - - [Fact] - public void GetOrDefault_should_return_default_and_not_add_it_if_key_not_exists() - { - Assert.Equal(0, valueDictionary.GetOrDefault(12)); - Assert.False(valueDictionary.ContainsKey(12)); - } - - [Fact] - public void GetOrAddDefault_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrAddDefault(12)); - } - - [Fact] - public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists() - { - Assert.Equal(0, valueDictionary.GetOrAddDefault(12)); - Assert.Equal(0, valueDictionary[12]); - } - - [Fact] - public void GetOrCreate_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34)); - } - - [Fact] - public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24)); - Assert.False(valueDictionary.ContainsKey(12)); - } - - [Fact] - public void GetOrAdd_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; - - Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 34)); - } - - [Fact] - public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrAdd(12, 24)); - Assert.Equal(24, valueDictionary[12]); - } - - [Fact] - public void GetOrAdd_should_return_default_and_add_it_with_fallback_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24)); - Assert.Equal(24, valueDictionary[12]); - } - - [Fact] - public void GetOrNew_should_return_value_if_key_exists() - { - var list = new List(); - listDictionary[12] = list; - - Assert.Equal(list, listDictionary.GetOrNew(12)); - } - - [Fact] - public void GetOrNew_should_return_default_but_not_add_it_if_key_not_exists() - { - var list = new List(); - - Assert.Equal(list, listDictionary.GetOrNew(12)); - Assert.False(listDictionary.ContainsKey(12)); - } - - [Fact] - public void GetOrAddNew_should_return_value_if_key_exists() - { - var list = new List(); - listDictionary[12] = list; - - Assert.Equal(list, listDictionary.GetOrAddNew(12)); - } - - [Fact] - public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists() - { - var list = new List(); - - Assert.Equal(list, listDictionary.GetOrAddNew(12)); - Assert.Equal(list, listDictionary[12]); - } - - [Fact] - public void SequentialHashCode_should_ignore_null_values() - { - var collection = new string[] { null, null }; - - Assert.Equal(17, collection.SequentialHashCode()); - } - - [Fact] - public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 5, 6 }; - - Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } - - [Fact] - public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 4, 1 }; - - Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } - - [Fact] - public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 6, 5, 3 }; - - Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } - - [Fact] - public void OrderedHashCode_should_return_same_hash_codes_for_list_with_same_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 5, 6 }; - - Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); - } - - [Fact] - public void OrderedHashCode_should_return_different_hash_codes_for_list_with_different_items() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 4, 1 }; - - Assert.NotEqual(collection2.OrderedHashCode(), collection1.OrderedHashCode()); - } - - [Fact] - public void OrderedHashCode_should_return_same_hash_codes_for_list_with_different_order() - { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 6, 5, 3 }; - - Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode()); - } - - [Fact] - public void EqualsDictionary_should_return_true_for_equal_dictionaries() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - - Assert.True(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void EqualsDictionary_should_return_false_for_different_sizes() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1 - }; - - Assert.False(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void EqualsDictionary_should_return_false_for_different_values() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [3] = 3 - }; - - Assert.False(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void Dictionary_should_return_same_hashcode_for_equal_dictionaries() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - - Assert.Equal(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); - } - - [Fact] - public void Dictionary_should_return_different_hashcode_for_different_dictionaries() - { - var lhs = new Dictionary - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary - { - [1] = 1, - [3] = 3 - }; - - Assert.NotEqual(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); - } - - [Fact] - public void Foreach_should_call_action_foreach_item() - { - var source = new List { 3, 5, 1 }; - var target = new List(); - - source.Foreach(target.Add); - - Assert.Equal(source, target); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs deleted file mode 100644 index c12893015..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Reflection; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class DomainObjectGrainFormatterTests - { - private readonly IGrainCallContext context = A.Fake(); - - [Fact] - public void Should_return_fallback_if_no_method_is_defined() - { - A.CallTo(() => context.InterfaceMethod) - .Returns(null); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("Unknown", result); - } - - [Fact] - public void Should_return_method_name_if_not_domain_object_method() - { - var methodInfo = A.Fake(); - - A.CallTo(() => methodInfo.Name) - .Returns("Calculate"); - - A.CallTo(() => context.InterfaceMethod) - .Returns(methodInfo); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("Calculate", result); - } - - [Fact] - public void Should_return_nice_method_name_if_domain_object_execute() - { - var methodInfo = A.Fake(); - - A.CallTo(() => methodInfo.Name) - .Returns(nameof(IDomainObjectGrain.ExecuteAsync)); - - A.CallTo(() => context.Arguments) - .Returns(new object[] { new MyCommand() }); - - A.CallTo(() => context.InterfaceMethod) - .Returns(methodInfo); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("ExecuteAsync(MyCommand)", result); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs deleted file mode 100644 index aaba71d95..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ /dev/null @@ -1,220 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class DomainObjectGrainTests - { - private readonly IStore store = A.Fake>(); - private readonly IPersistence persistence = A.Fake>(); - private readonly Guid id = Guid.NewGuid(); - private readonly MyDomainObject sut; - - public sealed class MyDomainObject : DomainObjectGrain - { - public MyDomainObject(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateAuto createAuto: - return Create(createAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case CreateCustom createCustom: - return CreateReturn(createCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "CREATED"; - }); - - case UpdateAuto updateAuto: - return Update(updateAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case UpdateCustom updateCustom: - return UpdateReturn(updateCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "UPDATED"; - }); - } - - return Task.FromResult(null); - } - } - - public DomainObjectGrainTests() - { - A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A.Ignored)) - .Returns(persistence); - - sut = new MyDomainObject(store); - } - - [Fact] - public void Should_instantiate() - { - Assert.Equal(EtagVersion.Empty, sut.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_created() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntityCreatedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_updated() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8))) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntitySavedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(8, sut.Snapshot.Value); - Assert.Equal(1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_throw_exception_when_already_created() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - } - - [Fact] - public async Task Should_throw_exception_when_not_created() - { - await SetupEmptyAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - } - - [Fact] - public async Task Should_return_custom_result_on_create() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateCustom())); - - Assert.Equal("CREATED", result.Value); - } - - [Fact] - public async Task Should_return_custom_result_on_update() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateCustom())); - - Assert.Equal("UPDATED", result.Value); - } - - [Fact] - public async Task Should_throw_exception_when_other_verison_expected() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_create_failed() - { - await SetupEmptyAsync(); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(0, sut.Snapshot.Value); - Assert.Equal(-1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_update_failed() - { - await SetupCreatedAsync(); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - private async Task SetupCreatedAsync() - { - await sut.ActivateAsync(id); - - await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - } - - private static J C(IAggregateCommand command) - { - return command.AsJ(); - } - - private async Task SetupEmptyAsync() - { - await sut.ActivateAsync(id); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs deleted file mode 100644 index 94f56de94..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class LogSnapshotDomainObjectGrainTests - { - private readonly IStore store = A.Fake>(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); - private readonly IPersistence persistence = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly MyLogDomainObject sut; - - public sealed class MyLogDomainObject : LogSnapshotDomainObjectGrain - { - public MyLogDomainObject(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateAuto createAuto: - return Create(createAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case CreateCustom createCustom: - return CreateReturn(createCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "CREATED"; - }); - - case UpdateAuto updateAuto: - return Update(updateAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case UpdateCustom updateCustom: - return UpdateReturn(updateCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "UPDATED"; - }); - } - - return Task.FromResult(null); - } - } - - public LogSnapshotDomainObjectGrainTests() - { - A.CallTo(() => store.WithEventSourcing(typeof(MyLogDomainObject), id, A.Ignored)) - .Returns(persistence); - - A.CallTo(() => store.GetSnapshotStore()) - .Returns(snapshotStore); - - sut = new MyLogDomainObject(store); - } - - [Fact] - public async Task Should_get_latestet_version_when_requesting_state_with_any() - { - await SetupUpdatedAsync(); - - var result = sut.GetSnapshot(EtagVersion.Any); - - result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); - } - - [Fact] - public async Task Should_get_latestet_version_when_requesting_state_with_auto() - { - await SetupUpdatedAsync(); - - var result = sut.GetSnapshot(EtagVersion.Auto); - - result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); - } - - [Fact] - public async Task Should_get_empty_version_when_requesting_state_with_empty_version() - { - await SetupUpdatedAsync(); - - var result = sut.GetSnapshot(EtagVersion.Empty); - - result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); - } - - [Fact] - public async Task Should_get_specific_version_when_requesting_state_with_specific_version() - { - await SetupUpdatedAsync(); - - sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 4, Version = 0 }); - sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); - } - - [Fact] - public async Task Should_get_null_state_when_requesting_state_with_invalid_version() - { - await SetupUpdatedAsync(); - - Assert.Null(sut.GetSnapshot(-4)); - Assert.Null(sut.GetSnapshot(2)); - } - - [Fact] - public void Should_instantiate() - { - Assert.Equal(EtagVersion.Empty, sut.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_created() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - - A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), -1, 0)) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntityCreatedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_updated() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - - A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 8), 0, 1)) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) - .MustHaveHappened(); - - Assert.True(result.Value is EntitySavedResult); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(8, sut.Snapshot.Value); - Assert.Equal(1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_throw_exception_when_already_created() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - } - - [Fact] - public async Task Should_throw_exception_when_not_created() - { - await SetupEmptyAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - } - - [Fact] - public async Task Should_return_custom_result_on_create() - { - await SetupEmptyAsync(); - - var result = await sut.ExecuteAsync(C(new CreateCustom())); - - Assert.Equal("CREATED", result.Value); - } - - [Fact] - public async Task Should_return_custom_result_on_update() - { - await SetupCreatedAsync(); - - var result = await sut.ExecuteAsync(C(new UpdateCustom())); - - Assert.Equal("UPDATED", result.Value); - } - - [Fact] - public async Task Should_throw_exception_when_other_verison_expected() - { - await SetupCreatedAsync(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_create_failed() - { - await SetupEmptyAsync(); - - A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, -1, 0)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(0, sut.Snapshot.Value); - Assert.Equal(-1, sut.Snapshot.Version); - } - - [Fact] - public async Task Should_reset_state_when_writing_snapshot_for_update_failed() - { - await SetupCreatedAsync(); - - A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, 0, 1)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); - - Assert.Empty(sut.GetUncomittedEvents()); - - Assert.Equal(4, sut.Snapshot.Value); - Assert.Equal(0, sut.Snapshot.Version); - } - - private async Task SetupCreatedAsync() - { - await sut.ActivateAsync(id); - - await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - } - - private async Task SetupUpdatedAsync() - { - await SetupCreatedAsync(); - - await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); - } - - private async Task SetupEmptyAsync() - { - await sut.ActivateAsync(id); - } - - private static J C(IAggregateCommand command) - { - return command.AsJ(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs deleted file mode 100644 index 54c7772ce..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs +++ /dev/null @@ -1,376 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public abstract class EventStoreTests where T : IEventStore - { - private readonly Lazy sut; - private string subscriptionPosition; - - public sealed class EventSubscriber : IEventSubscriber - { - public List Events { get; } = new List(); - - public string LastPosition { get; set; } - - public Task OnErrorAsync(IEventSubscription subscription, Exception exception) - { - throw new NotSupportedException(); - } - - public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - LastPosition = storedEvent.EventPosition; - - Events.Add(storedEvent); - - return TaskHelper.Done; - } - } - - protected T Sut - { - get { return sut.Value; } - } - - protected abstract int SubscriptionDelayInMs { get; } - - protected EventStoreTests() - { - sut = new Lazy(CreateStore); - } - - public abstract T CreateStore(); - - [Fact] - public async Task Should_throw_exception_for_version_mismatch() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); - } - - [Fact] - public async Task Should_throw_exception_for_version_mismatch_and_update() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - await Assert.ThrowsAsync(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, events)); - } - - [Fact] - public async Task Should_append_events() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - var readEvents1 = await QueryAsync(streamName); - var readEvents2 = await QueryWithCallbackAsync(streamName); - - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, events[0]), - new StoredEvent(streamName, "Position", 1, events[1]) - }; - - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } - - [Fact] - public async Task Should_subscribe_to_events() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - var readEvents = await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - }); - - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, events[0]), - new StoredEvent(streamName, "Position", 1, events[1]) - }; - - ShouldBeEquivalentTo(readEvents, expected); - } - - [Fact] - public async Task Should_subscribe_to_next_events() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events1 = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, events1); - }); - - var events2 = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - var readEventsFromPosition = await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, events2); - }); - - var expectedFromPosition = new[] - { - new StoredEvent(streamName, "Position", 2, events2[0]), - new StoredEvent(streamName, "Position", 3, events2[1]) - }; - - var readEventsFromBeginning = await QueryWithSubscriptionAsync(streamName, fromBeginning: true); - - var expectedFromBeginning = new[] - { - new StoredEvent(streamName, "Position", 0, events1[0]), - new StoredEvent(streamName, "Position", 1, events1[1]), - new StoredEvent(streamName, "Position", 2, events2[0]), - new StoredEvent(streamName, "Position", 3, events2[1]) - }; - - ShouldBeEquivalentTo(readEventsFromPosition, expectedFromPosition); - - ShouldBeEquivalentTo(readEventsFromBeginning, expectedFromBeginning); - } - - [Fact] - public async Task Should_read_events_from_offset() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - var firstRead = await QueryAsync(streamName); - - var readEvents1 = await QueryAsync(streamName, 1); - var readEvents2 = await QueryWithCallbackAsync(streamName, firstRead[0].EventPosition); - - var expected = new[] - { - new StoredEvent(streamName, "Position", 1, events[1]) - }; - - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } - - [Fact] - public async Task Should_delete_stream() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - new EventData("Type1", new EnvelopeHeaders(), "1"), - new EventData("Type2", new EnvelopeHeaders(), "2") - }; - - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - - await Sut.DeleteStreamAsync(streamName); - - var readEvents = await QueryAsync(streamName); - - Assert.Empty(readEvents); - } - - [Fact] - public async Task Should_query_events_by_property() - { - var keyed1 = new EnvelopeHeaders(); - var keyed2 = new EnvelopeHeaders(); - - keyed1.Add("key", Guid.NewGuid().ToString()); - keyed2.Add("key", Guid.NewGuid().ToString()); - - var streamName1 = $"test-{Guid.NewGuid()}"; - var streamName2 = $"test-{Guid.NewGuid()}"; - - var events1 = new[] - { - new EventData("Type1", keyed1, "1"), - new EventData("Type2", keyed2, "2") - }; - - var events2 = new[] - { - new EventData("Type3", keyed2, "3"), - new EventData("Type4", keyed1, "4") - }; - - await Sut.CreateIndexAsync("key"); - - await Sut.AppendAsync(Guid.NewGuid(), streamName1, events1); - await Sut.AppendAsync(Guid.NewGuid(), streamName2, events2); - - var readEvents = await QueryWithFilterAsync("key", keyed2["key"].ToString()); - - var expected = new[] - { - new StoredEvent(streamName1, "Position", 1, events1[1]), - new StoredEvent(streamName2, "Position", 0, events2[0]) - }; - - ShouldBeEquivalentTo(readEvents, expected); - } - - private Task> QueryAsync(string streamName, long position = EtagVersion.Any) - { - return Sut.QueryAsync(streamName, position); - } - - private async Task> QueryWithFilterAsync(string property, object value) - { - using (var cts = new CancellationTokenSource(30000)) - { - while (!cts.IsCancellationRequested) - { - var readEvents = new List(); - - await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, property, value, null, cts.Token); - - await Task.Delay(500, cts.Token); - - if (readEvents.Count > 0) - { - return readEvents; - } - } - - cts.Token.ThrowIfCancellationRequested(); - - return null; - } - } - - private async Task> QueryWithCallbackAsync(string streamFilter = null, string position = null) - { - using (var cts = new CancellationTokenSource(30000)) - { - while (!cts.IsCancellationRequested) - { - var readEvents = new List(); - - await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, streamFilter, position, cts.Token); - - await Task.Delay(500, cts.Token); - - if (readEvents.Count > 0) - { - return readEvents; - } - } - - cts.Token.ThrowIfCancellationRequested(); - - return null; - } - } - - private async Task> QueryWithSubscriptionAsync(string streamFilter, Func action = null, bool fromBeginning = false) - { - var subscriber = new EventSubscriber(); - - IEventSubscription subscription = null; - try - { - subscription = Sut.CreateSubscription(subscriber, streamFilter, fromBeginning ? null : subscriptionPosition); - - if (action != null) - { - await action(); - } - - using (var cts = new CancellationTokenSource(30000)) - { - while (!cts.IsCancellationRequested) - { - subscription.WakeUp(); - - await Task.Delay(500, cts.Token); - - if (subscriber.Events.Count > 0) - { - subscriptionPosition = subscriber.LastPosition; - - return subscriber.Events; - } - } - - cts.Token.ThrowIfCancellationRequested(); - - return null; - } - } - finally - { - await subscription.StopAsync(); - } - } - - private static void ShouldBeEquivalentTo(IEnumerable actual, params StoredEvent[] expected) - { - var actualArray = actual.Select(x => new StoredEvent(x.StreamName, "Position", x.EventStreamNumber, x.Data)).ToArray(); - - actualArray.Should().BeEquivalentTo(expected); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs deleted file mode 100644 index 71f4a6c56..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ /dev/null @@ -1,409 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Orleans.Concurrency; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerGrainTests - { - public sealed class MyEventConsumerGrain : EventConsumerGrain - { - public MyEventConsumerGrain( - EventConsumerFactory eventConsumerFactory, - IGrainState state, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - ISemanticLog log) - : base(eventConsumerFactory, state, eventStore, eventDataFormatter, log) - { - } - - protected override IEventConsumerGrain GetSelf() - { - return this; - } - - protected override IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position) - { - return store.CreateSubscription(subscriber, streamFilter, position); - } - } - - private readonly IGrainState grainState = A.Fake>(); - private readonly IEventConsumer eventConsumer = A.Fake(); - private readonly IEventStore eventStore = A.Fake(); - private readonly IEventSubscription eventSubscription = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly IEventDataFormatter formatter = A.Fake(); - private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - private readonly Envelope envelope = new Envelope(new MyEvent()); - private readonly EventConsumerGrain sut; - private readonly string consumerName; - private readonly string initialPosition = Guid.NewGuid().ToString(); - - public EventConsumerGrainTests() - { - grainState.Value.Position = initialPosition; - - consumerName = eventConsumer.GetType().Name; - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .Returns(eventSubscription); - - A.CallTo(() => eventConsumer.Name) - .Returns(consumerName); - - A.CallTo(() => eventConsumer.Handles(A.Ignored)) - .Returns(true); - - A.CallTo(() => formatter.Parse(eventData, null)) - .Returns(envelope); - - sut = new MyEventConsumerGrain( - x => eventConsumer, - grainState, - eventStore, - formatter, - log); - } - - [Fact] - public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() - { - grainState.Value = grainState.Value.Stopped(); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_subscribe_to_event_store_when_not_found_in_db() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_stop_subscription_when_stopped() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.StopAsync(); - await sut.StopAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_reset_consumer_when_resetting() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.StopAsync(); - await sut.ResetAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventConsumer.ClearAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, grainState.Value.Position)) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, null)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_invoke_and_update_position_when_event_received() - { - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_not_invoke_but_update_position_when_consumer_does_not_want_to_handle() - { - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - A.CallTo(() => eventConsumer.Handles(@event)) - .Returns(false); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_ignore_old_events() - { - A.CallTo(() => formatter.Parse(eventData, null)) - .Throws(new TypeNameNotFoundException()); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() - { - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(A.Fake(), @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_stop_if_consumer_failed() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - var ex = new InvalidOperationException(); - - await OnErrorAsync(eventSubscription, ex); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() - { - var ex = new InvalidOperationException(); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnErrorAsync(A.Fake(), ex); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_wakeup_when_already_subscribed() - { - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.ActivateAsync(); - - A.CallTo(() => eventSubscription.WakeUp()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_stop_if_resetting_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => eventConsumer.ClearAsync()) - .Throws(ex); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - await sut.ResetAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_stop_if_handling_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => eventConsumer.On(envelope)) - .Throws(ex); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_stop_if_deserialization_failed() - { - var ex = new InvalidOperationException(); - - A.CallTo(() => formatter.Parse(eventData, null)) - .Throws(ex); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - [Fact] - public async Task Should_start_after_stop_when_handling_failed() - { - var exception = new InvalidOperationException(); - - A.CallTo(() => eventConsumer.On(envelope)) - .Throws(exception); - - var @event = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - await sut.ActivateAsync(consumerName); - await sut.ActivateAsync(); - - await OnEventAsync(eventSubscription, @event); - - await sut.StopAsync(); - await sut.StartAsync(); - await sut.StartAsync(); - - grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(1, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(2, Times.Exactly); - } - - private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) - { - return sut.OnErrorAsync(subscriber.AsImmutable(), ex.AsImmutable()); - } - - private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) - { - return sut.OnEventAsync(subscriber.AsImmutable(), ev.AsImmutable()); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs deleted file mode 100644 index acf9c6a7a..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Orleans; -using Orleans.Concurrency; -using Orleans.Core; -using Orleans.Runtime; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerManagerGrainTests - { - public class MyEventConsumerManagerGrain : EventConsumerManagerGrain - { - public MyEventConsumerManagerGrain( - IEnumerable eventConsumers, - IGrainIdentity identity, - IGrainRuntime runtime) - : base(eventConsumers, identity, runtime) - { - } - } - - private readonly IEventConsumer consumerA = A.Fake(); - private readonly IEventConsumer consumerB = A.Fake(); - private readonly IEventConsumerGrain grainA = A.Fake(); - private readonly IEventConsumerGrain grainB = A.Fake(); - private readonly MyEventConsumerManagerGrain sut; - - public EventConsumerManagerGrainTests() - { - var grainRuntime = A.Fake(); - var grainFactory = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain("a", null)).Returns(grainA); - A.CallTo(() => grainFactory.GetGrain("b", null)).Returns(grainB); - A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); - - A.CallTo(() => consumerA.Name).Returns("a"); - A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); - - A.CallTo(() => consumerB.Name).Returns("b"); - A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); - - sut = new MyEventConsumerManagerGrain(new[] { consumerA, consumerB }, A.Fake(), grainRuntime); - } - - [Fact] - public async Task Should_not_activate_all_grains_on_activate() - { - await sut.OnActivateAsync(); - - A.CallTo(() => grainA.ActivateAsync()) - .MustNotHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_activate_all_grains_on_reminder() - { - await sut.ReceiveReminder(null, default); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_activate_all_grains_on_wakeup_with_null() - { - await sut.ActivateAsync(null); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_activate_matching_grains_when_stream_name_defined() - { - await sut.ActivateAsync("a-123"); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_start_all_grains() - { - await sut.StartAllAsync(); - - A.CallTo(() => grainA.StartAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StartAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_start_matching_grain() - { - await sut.StartAsync("a"); - - A.CallTo(() => grainA.StartAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StartAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_stop_all_grains() - { - await sut.StopAllAsync(); - - A.CallTo(() => grainA.StopAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StopAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_stop_matching_grain() - { - await sut.StopAsync("b"); - - A.CallTo(() => grainA.StopAsync()) - .MustNotHaveHappened(); - - A.CallTo(() => grainB.StopAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_reset_matching_grain() - { - await sut.ResetAsync("b"); - - A.CallTo(() => grainA.ResetAsync()) - .MustNotHaveHappened(); - - A.CallTo(() => grainB.ResetAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_fetch_infos_from_all_grains() - { - A.CallTo(() => grainA.GetStateAsync()) - .Returns(new Immutable( - new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); - - A.CallTo(() => grainB.GetStateAsync()) - .Returns(new Immutable( - new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); - - var infos = await sut.GetConsumersAsync(); - - infos.Value.Should().BeEquivalentTo( - new List - { - new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, - new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } - }); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs deleted file mode 100644 index 95d3759d0..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public class RetrySubscriptionTests - { - private readonly IEventStore eventStore = A.Fake(); - private readonly IEventSubscriber eventSubscriber = A.Fake(); - private readonly IEventSubscription eventSubscription = A.Fake(); - private readonly IEventSubscriber sutSubscriber; - private readonly RetrySubscription sut; - private readonly string streamFilter = Guid.NewGuid().ToString(); - - public RetrySubscriptionTests() - { - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)).Returns(eventSubscription); - - sut = new RetrySubscription(eventStore, eventSubscriber, streamFilter, null) { ReconnectWaitMs = 50 }; - - sutSubscriber = sut; - } - - [Fact] - public async Task Should_subscribe_after_constructor() - { - await sut.StopAsync(); - - A.CallTo(() => eventStore.CreateSubscription(sut, streamFilter, null)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_reopen_subscription_once_when_exception_is_retrieved() - { - await OnErrorAsync(eventSubscription, new InvalidOperationException()); - - await Task.Delay(1000); - - await sut.StopAsync(); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(2, Times.Exactly); - - A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_forward_error_from_inner_subscription_when_failed_often() - { - var ex = new InvalidOperationException(); - - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await OnErrorAsync(null, ex); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_forward_error_when_exception_is_from_another_subscription() - { - var ex = new InvalidOperationException(); - - await OnErrorAsync(A.Fake(), ex); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_forward_event_from_inner_subscription() - { - var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - - await OnEventAsync(eventSubscription, ev); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnEventAsync(sut, ev)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_forward_event_when_message_is_from_another_subscription() - { - var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - - await OnEventAsync(A.Fake(), ev); - await sut.StopAsync(); - - A.CallTo(() => eventSubscriber.OnEventAsync(A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) - { - return sutSubscriber.OnErrorAsync(subscriber, ex); - } - - private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) - { - return sutSubscriber.OnEventAsync(subscriber, ev); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/tests/Squidex.Infrastructure.Tests/GuardTests.cs deleted file mode 100644 index caabb05dc..000000000 --- a/tests/Squidex.Infrastructure.Tests/GuardTests.cs +++ /dev/null @@ -1,367 +0,0 @@ -// ========================================================================== -// 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.Validation; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class GuardTests - { - private sealed class MyValidatableValid : IValidatable - { - public void Validate(IList errors) - { - } - } - - private sealed class MyValidatableInvalid : IValidatable - { - public void Validate(IList errors) - { - errors.Add(new ValidationError("error.", "error")); - } - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - public void NotNullOrEmpty_should_throw_for_empy_strings(string invalidString) - { - Assert.Throws(() => Guard.NotNullOrEmpty(invalidString, "parameter")); - } - - [Fact] - public void NotNullOrEmpty_should_throw_for_null_string() - { - Assert.Throws(() => Guard.NotNullOrEmpty(null, "parameter")); - } - - [Fact] - public void NotNullOrEmpty_should_do_nothing_for_vaid_string() - { - Guard.NotNullOrEmpty("value", "parameter"); - } - - [Fact] - public void NotNull_should_throw_for_null_value() - { - Assert.Throws(() => Guard.NotNull(null, "parameter")); - } - - [Fact] - public void NotNull_should_do_nothing_for_valid_value() - { - Guard.NotNull("value", "parameter"); - } - - [Fact] - public void Enum_should_throw_for_invalid_enum() - { - Assert.Throws(() => Guard.Enum((DateTimeKind)13, "Parameter")); - } - - [Fact] - public void Enum_should_do_nothing_for_valid_enum() - { - Guard.Enum(DateTimeKind.Local, "Parameter"); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_guid() - { - Assert.Throws(() => Guard.NotEmpty(Guid.Empty, "parameter")); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_valid_guid() - { - Guard.NotEmpty(Guid.NewGuid(), "parameter"); - } - - [Fact] - public void HasType_should_throw_for_other_type() - { - Assert.Throws(() => Guard.HasType("value", "parameter")); - } - - [Fact] - public void HasType_should_do_nothing_for_null_value() - { - Guard.HasType(null, "parameter"); - } - - [Fact] - public void HasType_should_do_nothing_for_correct_type() - { - Guard.HasType(123, "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_throw_for_other_type() - { - Assert.Throws(() => Guard.HasType("value", typeof(int), "parameter")); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_null_value() - { - Guard.HasType(null, typeof(int), "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_correct_type() - { - Guard.HasType(123, typeof(int), "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_null_type() - { - Guard.HasType(123, null, "parameter"); - } - - [Fact] - public void NotDefault_should_throw_for_default_values() - { - Assert.Throws(() => Guard.NotDefault(Guid.Empty, "parameter")); - Assert.Throws(() => Guard.NotDefault(0, "parameter")); - Assert.Throws(() => Guard.NotDefault((string)null, "parameter")); - Assert.Throws(() => Guard.NotDefault(false, "parameter")); - } - - [Fact] - public void NotDefault_should_do_nothing_for_non_default_value() - { - Guard.NotDefault(Guid.NewGuid(), "parameter"); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" Not a Slug ")] - [InlineData(" not--a--slug ")] - [InlineData(" not-a-slug ")] - [InlineData("-not-a-slug-")] - [InlineData("not$-a-slug")] - [InlineData("not-a-Slug")] - public void ValidSlug_should_throw_for_invalid_slugs(string slug) - { - Assert.Throws(() => Guard.ValidSlug(slug, "parameter")); - } - - [Theory] - [InlineData("slug")] - [InlineData("slug23")] - [InlineData("other-slug")] - [InlineData("just-another-slug")] - public void ValidSlug_should_do_nothing_for_valid_slugs(string slug) - { - Guard.ValidSlug(slug, "parameter"); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" Not a Property ")] - [InlineData(" not--a--property ")] - [InlineData(" not-a-property ")] - [InlineData("-not-a-property-")] - [InlineData("not$-a-property")] - public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) - { - Assert.Throws(() => Guard.ValidPropertyName(slug, "property")); - } - - [Theory] - [InlineData("property")] - [InlineData("property23")] - [InlineData("other-property")] - [InlineData("other-Property")] - [InlineData("otherProperty")] - [InlineData("just-another-property")] - [InlineData("just-Another-Property")] - [InlineData("justAnotherProperty")] - public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) - { - Guard.ValidPropertyName(property, "parameter"); - } - - [Theory] - [InlineData(double.PositiveInfinity)] - [InlineData(double.NegativeInfinity)] - [InlineData(double.NaN)] - public void ValidNumber_should_throw_for_invalid_doubles(double value) - { - Assert.Throws(() => Guard.ValidNumber(value, "parameter")); - } - - [Theory] - [InlineData(0d)] - [InlineData(-1000d)] - [InlineData(1000d)] - public void ValidNumber_do_nothing_for_valid_double(double value) - { - Guard.ValidNumber(value, "parameter"); - } - - [Theory] - [InlineData(float.PositiveInfinity)] - [InlineData(float.NegativeInfinity)] - [InlineData(float.NaN)] - public void ValidNumber_should_throw_for_invalid_float(float value) - { - Assert.Throws(() => Guard.ValidNumber(value, "parameter")); - } - - [Theory] - [InlineData(0f)] - [InlineData(-1000f)] - [InlineData(1000f)] - public void ValidNumber_do_nothing_for_valid_float(float value) - { - Guard.ValidNumber(value, "parameter"); - } - - [Theory] - [InlineData(4)] - [InlineData(104)] - public void Between_should_throw_for_values_outside_of_range(int value) - { - Assert.Throws(() => Guard.Between(value, 10, 100, "parameter")); - } - - [Theory] - [InlineData(10)] - [InlineData(55)] - [InlineData(100)] - public void Between_should_do_nothing_for_values_in_range(int value) - { - Guard.Between(value, 10, 100, "parameter"); - } - - [Theory] - [InlineData(0)] - [InlineData(100)] - public void GreaterThan_should_throw_for_smaller_values(int value) - { - Assert.Throws(() => Guard.GreaterThan(value, 100, "parameter")); - } - - [Theory] - [InlineData(101)] - [InlineData(200)] - public void GreaterThan_should_do_nothing_for_greater_values(int value) - { - Guard.GreaterThan(value, 100, "parameter"); - } - - [Theory] - [InlineData(0)] - [InlineData(99)] - public void GreaterEquals_should_throw_for_smaller_values(int value) - { - Assert.Throws(() => Guard.GreaterEquals(value, 100, "parameter")); - } - - [Theory] - [InlineData(100)] - [InlineData(200)] - public void GreaterEquals_should_do_nothing_for_greater_values(int value) - { - Guard.GreaterEquals(value, 100, "parameter"); - } - - [Theory] - [InlineData(1000)] - [InlineData(100)] - public void LessThan_should_throw_for_greater_values(int value) - { - Assert.Throws(() => Guard.LessThan(value, 100, "parameter")); - } - - [Theory] - [InlineData(99)] - [InlineData(50)] - public void LessThan_should_do_nothing_for_smaller_values(int value) - { - Guard.LessThan(value, 100, "parameter"); - } - - [Theory] - [InlineData(1000)] - [InlineData(101)] - public void LessEquals_should_throw_for_greater_values(int value) - { - Assert.Throws(() => Guard.LessEquals(value, 100, "parameter")); - } - - [Theory] - [InlineData(100)] - [InlineData(50)] - public void LessEquals_should_do_nothing_for_smaller_values(int value) - { - Guard.LessEquals(value, 100, "parameter"); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_collection() - { - Assert.Throws(() => Guard.NotEmpty(new int[0], "parameter")); - } - - [Fact] - public void NotEmpty_should_throw_for_null_collection() - { - Assert.Throws(() => Guard.NotEmpty((int[])null, "parameter")); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_value_collection() - { - Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); - } - - [Fact] - public void ValidFileName_should_throw_for_invalid_file_name() - { - Assert.Throws(() => Guard.ValidFileName("File/Name", "Parameter")); - } - - [Fact] - public void ValidFileName_should_throw_for_null_file_name() - { - Assert.Throws(() => Guard.ValidFileName(null, "Parameter")); - } - - [Fact] - public void ValidFileName_should_do_nothing_for_valid_file_name() - { - Guard.ValidFileName("FileName", "Parameter"); - } - - [Fact] - public void Valid_should_throw_exception_if_null() - { - Assert.Throws(() => Guard.Valid(null, "Parameter", () => "Message")); - } - - [Fact] - public void Valid_should_throw_exception_if_invalid() - { - Assert.Throws(() => Guard.Valid(new MyValidatableInvalid(), "Parameter", () => "Message")); - } - - [Fact] - public void Valid_should_do_nothing_if_valid() - { - Guard.Valid(new MyValidatableValid(), "Parameter", () => "Message"); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs deleted file mode 100644 index e84f11584..000000000 --- a/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using Xunit; - -#pragma warning disable SA1122 // Use string.Empty for empty strings - -namespace Squidex.Infrastructure.Http -{ - public class DumpFormatterTests - { - [Fact] - public void Should_format_dump_without_response() - { - var httpRequest = CreateRequest(); - - var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/2.0", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "", - "", - "Response:", - "Timeout after 00:01:00"); - - Assert.Equal(expected, dump); - } - - [Fact] - public void Should_format_dump_without_content() - { - var httpRequest = CreateRequest(); - var httpResponse = CreateResponse(); - - var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/2.0", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "", - "", - "Response:", - "HTTP/1.1 200 OK", - "Transfer-Encoding: UTF-8", - "Trailer: Expires", - "", - "Elapsed: 00:01:00"); - - Assert.Equal(expected, dump); - } - - [Fact] - public void Should_format_dump_with_content_without_timeout() - { - var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); - var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); - - var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/2.0", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "Content-Type: text/plain; charset=utf-8", - "", - "Hello Squidex", - "", - "", - "Response:", - "HTTP/1.1 200 OK", - "Transfer-Encoding: UTF-8", - "Trailer: Expires", - "Content-Type: text/plain; charset=utf-8", - "", - "Hello Back", - "", - "Elapsed: 00:01:00"); - - Assert.Equal(expected, dump); - } - - private static HttpRequestMessage CreateRequest(HttpContent content = null) - { - var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); - - request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); - request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); - - request.Content = content; - - return request; - } - - private static HttpResponseMessage CreateResponse(HttpContent content = null) - { - var response = new HttpResponseMessage(HttpStatusCode.OK); - - response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); - response.Headers.Trailer.Add("Expires"); - - response.Content = content; - - return response; - } - - private static string CreateExpectedDump(params string[] input) - { - return string.Join(Environment.NewLine, input) + Environment.NewLine; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs deleted file mode 100644 index cf1e98f0c..000000000 --- a/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq; -using System.Security.Claims; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Json -{ - public class ClaimsPrincipalConverterTests - { - [Fact] - public void Should_serialize_and_deserialize() - { - var value = new ClaimsPrincipal( - new[] - { - new ClaimsIdentity( - new[] - { - new Claim("email", "me@email.com"), - new Claim("username", "me@email.com") - }, - "Cookie"), - new ClaimsIdentity( - new[] - { - new Claim("user_id", "12345"), - new Claim("login", "me") - }, - "Google") - }); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, serialized.Identities.ElementAt(0).AuthenticationType); - Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, serialized.Identities.ElementAt(1).AuthenticationType); - } - - [Fact] - public void Should_serialize_and_deserialize_null_principal() - { - ClaimsPrincipal value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Null(serialized); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs deleted file mode 100644 index 5ad034901..000000000 --- a/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs +++ /dev/null @@ -1,357 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using NodaTime; -using Xunit; - -namespace Squidex.Infrastructure.Json.Objects -{ - public class JsonObjectTests - { - [Fact] - public void Should_make_correct_object_equal_comparisons() - { - var obj_count1_key1_val1_a = JsonValue.Object().Add("key1", 1); - var obj_count1_key1_val1_b = JsonValue.Object().Add("key1", 1); - - var obj_count1_key1_val2 = JsonValue.Object().Add("key1", 2); - var obj_count1_key2_val1 = JsonValue.Object().Add("key2", 1); - var obj_count2_key1_val1 = JsonValue.Object().Add("key1", 1).Add("key2", 2); - - var number = JsonValue.Create(1); - - Assert.Equal(obj_count1_key1_val1_a, obj_count1_key1_val1_b); - Assert.Equal(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val1_b.GetHashCode()); - Assert.True(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val1_b)); - - Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key1_val2); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key1_val2.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key1_val2)); - - Assert.NotEqual(obj_count1_key1_val1_a, obj_count1_key2_val1); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count1_key2_val1.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count1_key2_val1)); - - Assert.NotEqual(obj_count1_key1_val1_a, obj_count2_key1_val1); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), obj_count2_key1_val1.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)obj_count2_key1_val1)); - - Assert.NotEqual(obj_count1_key1_val1_a, number); - Assert.NotEqual(obj_count1_key1_val1_a.GetHashCode(), number.GetHashCode()); - Assert.False(obj_count1_key1_val1_a.Equals((object)number)); - } - - [Fact] - public void Should_make_correct_array_equal_comparisons() - { - var array_count1_val1_a = JsonValue.Array(1); - var array_count1_val1_b = JsonValue.Array(1); - - var array_count1_val2 = JsonValue.Array(2); - var array_count2_val1 = JsonValue.Array(1, 2); - - var number = JsonValue.Create(1); - - Assert.Equal(array_count1_val1_a, array_count1_val1_b); - Assert.Equal(array_count1_val1_a.GetHashCode(), array_count1_val1_b.GetHashCode()); - Assert.True(array_count1_val1_a.Equals((object)array_count1_val1_b)); - - Assert.NotEqual(array_count1_val1_a, array_count1_val2); - Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count1_val2.GetHashCode()); - Assert.False(array_count1_val1_a.Equals((object)array_count1_val2)); - - Assert.NotEqual(array_count1_val1_a, array_count2_val1); - Assert.NotEqual(array_count1_val1_a.GetHashCode(), array_count2_val1.GetHashCode()); - Assert.False(array_count1_val1_a.Equals((object)array_count2_val1)); - - Assert.NotEqual(array_count1_val1_a, number); - Assert.NotEqual(array_count1_val1_a.GetHashCode(), number.GetHashCode()); - Assert.False(array_count1_val1_a.Equals((object)number)); - } - - [Fact] - public void Should_make_correct_array_scalar_comparisons() - { - var number_val1_a = JsonValue.Create(1); - var number_val1_b = JsonValue.Create(1); - - var number_val2 = JsonValue.Create(2); - - var boolean = JsonValue.True; - - Assert.Equal(number_val1_a, number_val1_b); - Assert.Equal(number_val1_a.GetHashCode(), number_val1_b.GetHashCode()); - Assert.True(number_val1_a.Equals((object)number_val1_b)); - - Assert.NotEqual(number_val1_a, number_val2); - Assert.NotEqual(number_val1_a.GetHashCode(), number_val2.GetHashCode()); - Assert.False(number_val1_a.Equals((object)number_val2)); - - Assert.NotEqual(number_val1_a, boolean); - Assert.NotEqual(number_val1_a.GetHashCode(), boolean.GetHashCode()); - Assert.False(number_val1_a.Equals((object)boolean)); - } - - [Fact] - public void Should_make_correct_null_comparisons() - { - var null_a = JsonValue.Null; - var null_b = JsonValue.Null; - - var boolean = JsonValue.True; - - Assert.Equal(null_a, null_b); - Assert.Equal(null_a.GetHashCode(), null_b.GetHashCode()); - Assert.True(null_a.Equals((object)null_b)); - - Assert.NotEqual(null_a, boolean); - Assert.NotEqual(null_a.GetHashCode(), boolean.GetHashCode()); - Assert.False(null_a.Equals((object)boolean)); - } - - [Fact] - public void Should_cache_null() - { - Assert.Same(JsonValue.Null, JsonValue.Create((string)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((bool?)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((double?)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((object)null)); - Assert.Same(JsonValue.Null, JsonValue.Create((Instant?)null)); - } - - [Fact] - public void Should_cache_true() - { - Assert.Same(JsonValue.True, JsonValue.Create(true)); - } - - [Fact] - public void Should_cache_false() - { - Assert.Same(JsonValue.False, JsonValue.Create(false)); - } - - [Fact] - public void Should_cache_empty() - { - Assert.Same(JsonValue.Empty, JsonValue.Create(string.Empty)); - } - - [Fact] - public void Should_cache_zero() - { - Assert.Same(JsonValue.Zero, JsonValue.Create(0)); - } - - [Fact] - public void Should_boolean_from_object() - { - Assert.Equal(JsonValue.True, JsonValue.Create((object)true)); - } - - [Fact] - public void Should_create_value_from_instant() - { - var instant = Instant.FromUnixTimeSeconds(4123125455); - - Assert.Equal(instant.ToString(), JsonValue.Create(instant).ToString()); - } - - [Fact] - public void Should_create_value_from_instant_object() - { - var instant = Instant.FromUnixTimeSeconds(4123125455); - - Assert.Equal(instant.ToString(), JsonValue.Create((object)instant).ToString()); - } - - [Fact] - public void Should_create_array() - { - var json = JsonValue.Array(1, "2"); - - Assert.Equal("[1, \"2\"]", json.ToJsonString()); - Assert.Equal("[1, \"2\"]", json.ToString()); - } - - [Fact] - public void Should_create_object() - { - var json = JsonValue.Object().Add("key1", 1).Add("key2", "2"); - - Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToJsonString()); - Assert.Equal("{\"key1\":1, \"key2\":\"2\"}", json.ToString()); - } - - [Fact] - public void Should_create_number() - { - var json = JsonValue.Create(123); - - Assert.Equal("123", json.ToJsonString()); - Assert.Equal("123", json.ToString()); - } - - [Fact] - public void Should_create_boolean_true() - { - var json = JsonValue.Create(true); - - Assert.Equal("true", json.ToJsonString()); - Assert.Equal("true", json.ToString()); - } - - [Fact] - public void Should_create_boolean_false() - { - var json = JsonValue.Create(false); - - Assert.Equal("false", json.ToJsonString()); - Assert.Equal("false", json.ToString()); - } - - [Fact] - public void Should_create_string() - { - var json = JsonValue.Create("hi"); - - Assert.Equal("\"hi\"", json.ToJsonString()); - Assert.Equal("hi", json.ToString()); - } - - [Fact] - public void Should_create_null() - { - var json = JsonValue.Create((object)null); - - Assert.Equal("null", json.ToJsonString()); - Assert.Equal("null", json.ToString()); - } - - [Fact] - public void Should_create_arrays_in_different_ways() - { - var numbers = new[] - { - JsonValue.Array(1.0f, 2.0f), - JsonValue.Array(JsonValue.Create(1.0f), JsonValue.Create(2.0f)) - }; - - Assert.Single(numbers.Distinct()); - Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); - } - - [Fact] - public void Should_create_number_from_types() - { - var numbers = new[] - { - JsonValue.Create(12.0f), - JsonValue.Create(12.0), - JsonValue.Create(12L), - JsonValue.Create(12), - JsonValue.Create((object)12.0d), - JsonValue.Create((double?)12.0d) - }; - - Assert.Single(numbers.Distinct()); - Assert.Single(numbers.Select(x => x.GetHashCode()).Distinct()); - } - - [Fact] - public void Should_create_null_when_adding_null_to_array() - { - var array = JsonValue.Array(); - - array.Add(null); - - Assert.Same(JsonValue.Null, array[0]); - } - - [Fact] - public void Should_create_null_when_replacing_to_null_in_array() - { - var array = JsonValue.Array(1); - - array[0] = null; - - Assert.Same(JsonValue.Null, array[0]); - } - - [Fact] - public void Should_create_null_when_adding_null_to_object() - { - var obj = JsonValue.Object(); - - obj.Add("key", null); - - Assert.Same(JsonValue.Null, obj["key"]); - } - - [Fact] - public void Should_create_null_when_replacing_to_null_object() - { - var obj = JsonValue.Object(); - - obj["key"] = null; - - Assert.Same(JsonValue.Null, obj["key"]); - } - - [Fact] - public void Should_remove_value_from_object() - { - var obj = JsonValue.Object().Add("key", 1); - - obj.Remove("key"); - - Assert.False(obj.TryGetValue("key", out _)); - Assert.False(obj.ContainsKey("key")); - } - - [Fact] - public void Should_clear_values_from_object() - { - var obj = JsonValue.Object().Add("key", 1); - - obj.Clear(); - - Assert.False(obj.TryGetValue("key", out _)); - Assert.False(obj.ContainsKey("key")); - } - - [Fact] - public void Should_provide_collection_values_from_object() - { - var obj = JsonValue.Object().Add("11", "44").Add("22", "88"); - - var kvps = new[] - { - new KeyValuePair("11", JsonValue.Create("44")), - new KeyValuePair("22", JsonValue.Create("88")) - }; - - Assert.Equal(2, obj.Count); - - Assert.Equal(new[] { "11", "22" }, obj.Keys); - Assert.Equal(new[] { "44", "88" }, obj.Values.Select(x => x.ToString())); - - Assert.Equal(kvps, obj.ToArray()); - Assert.Equal(kvps, ((IEnumerable)obj).OfType>().ToArray()); - } - - [Fact] - public void Should_throw_exception_when_creation_value_from_invalid_type() - { - Assert.Throws(() => JsonValue.Create(Guid.Empty)); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs deleted file mode 100644 index c02983749..000000000 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class LanguageTests - { - [Theory] - [InlineData("")] - [InlineData(" ")] - public void Should_throw_exception_if_getting_by_empty_key(string key) - { - Assert.Throws(() => Language.GetLanguage(key)); - } - - [Fact] - public void Should_throw_exception_if_getting_by_null_key() - { - Assert.Throws(() => Language.GetLanguage(null)); - } - - [Fact] - public void Should_throw_exception_if_getting_by_unsupported_language() - { - Assert.Throws(() => Language.GetLanguage("xy")); - } - - [Fact] - public void Should_provide_all_languages() - { - Assert.True(Language.AllLanguages.Count > 100); - } - - [Fact] - public void Should_return_true_for_valid_language() - { - Assert.True(Language.IsValidLanguage("de")); - } - - [Fact] - public void Should_return_false_for_invalid_language() - { - Assert.False(Language.IsValidLanguage("xx")); - } - - [Fact] - public void Should_make_implicit_conversion_to_language() - { - Language language = "de"; - - Assert.Equal(Language.DE, language); - } - - [Fact] - public void Should_make_implicit_conversion_to_string() - { - string iso2Code = Language.DE; - - Assert.Equal("de", iso2Code); - } - - [Theory] - [InlineData("de", "German")] - [InlineData("en", "English")] - [InlineData("sv", "Swedish")] - [InlineData("zh", "Chinese")] - public void Should_provide_correct_english_name(string key, string englishName) - { - var language = Language.GetLanguage(key); - - Assert.Equal(key, language.Iso2Code); - Assert.Equal(englishName, language.EnglishName); - Assert.Equal(englishName, language.ToString()); - } - - [Theory] - [InlineData("en", "en")] - [InlineData("en ", "en")] - [InlineData("EN", "en")] - [InlineData("EN ", "en")] - public void Should_parse_valid_languages(string input, string languageCode) - { - var language = Language.ParseOrNull(input); - - Assert.Equal(language, Language.GetLanguage(languageCode)); - } - - [Theory] - [InlineData("en-US", "en")] - [InlineData("en-GB", "en")] - [InlineData("EN-US", "en")] - [InlineData("EN-GB", "en")] - public void Should_parse_lanuages_from_culture(string input, string languageCode) - { - var language = Language.ParseOrNull(input); - - Assert.Equal(language, Language.GetLanguage(languageCode)); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData("xx")] - [InlineData("invalid")] - [InlineData(null)] - public void Should_parse_invalid_languages(string input) - { - var language = Language.ParseOrNull(input); - - Assert.Null(language); - } - - [Fact] - public void Should_serialize_and_deserialize_null_language() - { - Language value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_language() - { - var value = Language.DE; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs b/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs deleted file mode 100644 index 62cd813c5..000000000 --- a/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Squidex.Infrastructure -{ - public sealed class LanguagesInitializerTests - { - [Fact] - public async Task Should_add_custom_languages() - { - var options = Options.Create(new LanguagesOptions - { - ["en-NO"] = "English (Norwegian)" - }); - - var sut = new LanguagesInitializer(options); - - await sut.InitializeAsync(); - - Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); - } - - [Fact] - public async Task Should_not_add_invalid_languages() - { - var options = Options.Create(new LanguagesOptions - { - ["en-Error"] = null - }); - - var sut = new LanguagesInitializer(options); - - await sut.InitializeAsync(); - - Assert.False(Language.TryGetLanguage("en-Error", out _)); - } - - [Fact] - public async Task Should_not_override_existing_languages() - { - var options = Options.Create(new LanguagesOptions - { - ["de"] = "German (Germany)" - }); - - var sut = new LanguagesInitializer(options); - - await sut.InitializeAsync(); - - Assert.Equal("German", Language.GetLanguage("de").EnglishName); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs deleted file mode 100644 index f33a19c3e..000000000 --- a/tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Infrastructure.Log -{ - public class LockingLogStoreTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ILockGrain lockGrain = A.Fake(); - private readonly ILogStore inner = A.Fake(); - private readonly LockingLogStore sut; - - public LockingLogStoreTests() - { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(lockGrain); - - sut = new LockingLogStore(inner, grainFactory); - } - - [Fact] - public async Task Should_lock_and_call_inner() - { - var stream = new MemoryStream(); - - var dateFrom = DateTime.Today; - var dateTo = dateFrom.AddDays(2); - - var key = "MyKey"; - - var releaseToken = Guid.NewGuid().ToString(); - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .Returns(releaseToken); - - await sut.ReadLogAsync(key, dateFrom, dateTo, stream); - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .MustHaveHappened(); - - A.CallTo(() => lockGrain.ReleaseLockAsync(releaseToken)) - .MustHaveHappened(); - - A.CallTo(() => inner.ReadLogAsync(key, dateFrom, dateTo, stream)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_write_default_message_if_lock_could_not_be_acquired() - { - var stream = new MemoryStream(); - - var dateFrom = DateTime.Today; - var dateTo = dateFrom.AddDays(2); - - var key = "MyKey"; - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .Returns(Task.FromResult(null)); - - await sut.ReadLogAsync(key, dateFrom, dateTo, stream, TimeSpan.FromSeconds(1)); - - A.CallTo(() => lockGrain.AcquireLockAsync(key)) - .MustHaveHappened(); - - A.CallTo(() => lockGrain.ReleaseLockAsync(A.Ignored)) - .MustNotHaveHappened(); - - A.CallTo(() => inner.ReadLogAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - - Assert.True(stream.Length > 0); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs deleted file mode 100644 index fd4b8566d..000000000 --- a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs +++ /dev/null @@ -1,525 +0,0 @@ -// ========================================================================== -// 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 FakeItEasy; -using Microsoft.Extensions.Logging; -using NodaTime; -using Squidex.Infrastructure.Log.Adapter; -using Xunit; - -namespace Squidex.Infrastructure.Log -{ - public class SemanticLogTests - { - private readonly List appenders = new List(); - private readonly List channels = new List(); - private readonly Lazy log; - private readonly ILogChannel channel = A.Fake(); - private string output = string.Empty; - - public SemanticLog Log - { - get { return log.Value; } - } - - public SemanticLogTests() - { - channels.Add(channel); - - A.CallTo(() => channel.Log(A.Ignored, A.Ignored)) - .Invokes((SemanticLogLevel level, string message) => - { - output += message; - }); - - log = new Lazy(() => new SemanticLog(channels, appenders, JsonLogWriterFactory.Default())); - } - - [Fact] - public void Should_log_multiple_lines() - { - Log.Log(SemanticLogLevel.Error, null, (_, w) => w.WriteProperty("logMessage", "Msg1")); - Log.Log(SemanticLogLevel.Error, null, (_, w) => w.WriteProperty("logMessage", "Msg2")); - - var expected1 = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logMessage", "Msg1")); - - var expected2 = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logMessage", "Msg2")); - - Assert.Equal(expected1 + expected2, output); - } - - [Fact] - public void Should_log_timestamp() - { - var clock = A.Fake(); - - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - - appenders.Add(new TimestampLogAppender(clock)); - - Log.LogFatal(w => { /* Do Nothing */ }); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("timestamp", clock.GetCurrentInstant())); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_values_with_appender() - { - appenders.Add(new ConstantsLogWriter(w => w.WriteProperty("logValue", 1500))); - - Log.LogFatal(m => { }); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_application_info() - { - var sessionId = Guid.NewGuid(); - - appenders.Add(new ApplicationInfoLogAppender(GetType(), sessionId)); - - Log.LogFatal(m => { }); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteObject("app", a => a - .WriteProperty("name", "Squidex.Infrastructure.Tests") - .WriteProperty("version", "1.0.0.0") - .WriteProperty("sessionId", sessionId.ToString()))); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_trace() - { - Log.LogTrace(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_trace_and_context() - { - Log.LogTrace(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_debug() - { - Log.LogDebug(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_debug_and_context() - { - Log.LogDebug(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_information() - { - Log.LogInformation(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_information_and_context() - { - Log.LogInformation(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning() - { - Log.LogWarning(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning_and_context() - { - Log.LogWarning(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning_exception() - { - var exception = new InvalidOperationException(); - - Log.LogWarning(exception); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_warning_exception_and_context() - { - var exception = new InvalidOperationException(); - - Log.LogWarning(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Warning") - .WriteProperty("logValue", 1500) - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error() - { - Log.LogError(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error_and_context() - { - Log.LogError(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error_exception() - { - var exception = new InvalidOperationException(); - - Log.LogError(exception); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_error_exception_and_context() - { - var exception = new InvalidOperationException(); - - Log.LogError(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Error") - .WriteProperty("logValue", 1500) - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal() - { - Log.LogFatal(w => w.WriteProperty("logValue", 1500)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal_and_context() - { - Log.LogFatal(1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal_exception() - { - var exception = new InvalidOperationException(); - - Log.LogFatal(exception); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_with_fatal_exception_and_context() - { - var exception = new InvalidOperationException(); - - Log.LogFatal(exception, 1500, (ctx, w) => w.WriteProperty("logValue", ctx)); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("logValue", 1500) - .WriteException(exception)); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_log_nothing_when_exception_is_null() - { - Log.LogFatal((Exception)null); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal")); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_measure_trace() - { - Log.MeasureTrace(w => w.WriteProperty("message", "My Message")).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_trace_with_contex() - { - Log.MeasureTrace("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Trace") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_debug() - { - Log.MeasureDebug(w => w.WriteProperty("message", "My Message")).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_debug_with_contex() - { - Log.MeasureDebug("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Debug") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_information() - { - Log.MeasureInformation(w => w.WriteProperty("message", "My Message")).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_measure_information_with_contex() - { - Log.MeasureInformation("My Message", (ctx, w) => w.WriteProperty("message", ctx)).Dispose(); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Information") - .WriteProperty("message", "My Message") - .WriteProperty("elapsedMs", 0)); - - Assert.StartsWith(expected.Substring(0, 55), output, StringComparison.Ordinal); - } - - [Fact] - public void Should_log_with_extensions_logger() - { - var exception = new InvalidOperationException(); - - var loggerFactory = - new LoggerFactory() - .AddSemanticLog(Log); - var loggerInstance = loggerFactory.CreateLogger(); - - loggerInstance.LogCritical(new EventId(123, "EventName"), exception, "Log {0}", 123); - - var expected = - LogTest(w => w - .WriteProperty("logLevel", "Fatal") - .WriteProperty("message", "Log 123") - .WriteObject("eventId", e => e - .WriteProperty("id", 123) - .WriteProperty("name", "EventName")) - .WriteException(exception) - .WriteProperty("category", "Squidex.Infrastructure.Log.SemanticLogTests")); - - Assert.Equal(expected, output); - } - - [Fact] - public void Should_catch_all_exceptions_from_all_channels_when_exceptions_are_thrown() - { - var exception1 = new InvalidOperationException(); - var exception2 = new InvalidOperationException(); - - var channel1 = A.Fake(); - var channel2 = A.Fake(); - - A.CallTo(() => channel1.Log(A.Ignored, A.Ignored)).Throws(exception1); - A.CallTo(() => channel2.Log(A.Ignored, A.Ignored)).Throws(exception2); - - var sut = new SemanticLog(new[] { channel1, channel2 }, Enumerable.Empty(), JsonLogWriterFactory.Default()); - - try - { - sut.Log(SemanticLogLevel.Debug, null, (_, w) => w.WriteProperty("should", "throw")); - - Assert.False(true); - } - catch (AggregateException ex) - { - Assert.Equal(exception1, ex.InnerExceptions[0]); - Assert.Equal(exception2, ex.InnerExceptions[1]); - } - } - - private static string LogTest(Action writer) - { - var sut = JsonLogWriterFactory.Default().Create(); - - writer(sut); - - return sut.ToString(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs deleted file mode 100644 index 6237b3b04..000000000 --- a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// 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 System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Migrations -{ - public class MigratorTests - { - private readonly IMigrationStatus status = A.Fake(); - private readonly IMigrationPath path = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); - - public sealed class InMemoryStatus : IMigrationStatus - { - private readonly object lockObject = new object(); - private int version; - private bool isLocked; - - public Task GetVersionAsync() - { - return Task.FromResult(version); - } - - public Task TryLockAsync() - { - var lockAcquired = false; - - lock (lockObject) - { - if (!isLocked) - { - isLocked = true; - - lockAcquired = true; - } - } - - return Task.FromResult(lockAcquired); - } - - public Task UnlockAsync(int newVersion) - { - lock (lockObject) - { - isLocked = false; - - version = newVersion; - } - - return TaskHelper.Done; - } - } - - public MigratorTests() - { - A.CallTo(() => path.GetNext(A.Ignored)) - .ReturnsLazily((int v) => - { - var m = migrations.Where(x => x.From == v).ToList(); - - return m.Count == 0 ? (0, null) : (migrations.Max(x => x.To), migrations.Select(x => x.Migration)); - }); - - A.CallTo(() => status.GetVersionAsync()).Returns(0); - A.CallTo(() => status.TryLockAsync()).Returns(true); - } - - [Fact] - public async Task Should_migrate_step_by_step() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); - - var sut = new Migrator(status, path, log); - - await sut.MigrateAsync(); - - A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); - - A.CallTo(() => status.UnlockAsync(3)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_unlock_when_migration_failed() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); - - var sut = new Migrator(status, path, log); - - A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); - - await Assert.ThrowsAsync(() => sut.MigrateAsync()); - - A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); - - A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); - } - - [Fact] - public async Task Should_log_exception_when_migration_failed() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - - var ex = new InvalidOperationException(); - - A.CallTo(() => migrator_0_1.UpdateAsync()) - .Throws(ex); - - var sut = new Migrator(status, path, log); - - await Assert.ThrowsAsync(() => sut.MigrateAsync()); - - A.CallTo(() => log.Log(SemanticLogLevel.Fatal, default, A>.Ignored)) - .MustHaveHappened(); - - A.CallTo(() => migrator_1_2.UpdateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_prevent_multiple_updates() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - - var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; - - await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); - - A.CallTo(() => migrator_0_1.UpdateAsync()) - .MustHaveHappened(1, Times.Exactly); - A.CallTo(() => migrator_1_2.UpdateAsync()) - .MustHaveHappened(1, Times.Exactly); - } - - private IMigration BuildMigration(int fromVersion, int toVersion) - { - var migration = A.Fake(); - - migrations.Add((fromVersion, toVersion, migration)); - - return migration; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs deleted file mode 100644 index 33b8da4cf..000000000 --- a/tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.MongoDb -{ - public class MongoExtensionsTests - { - public sealed class Cursor : IAsyncCursor - { - private readonly List items = new List(); - private int index = -1; - - public IEnumerable Current - { - get - { - if (items[index] is Exception ex) - { - throw ex; - } - - return Enumerable.Repeat((T)items[index], 1); - } - } - - public Cursor Add(params T[] newItems) - { - foreach (var item in newItems) - { - items.Add(item); - } - - return this; - } - - public Cursor Add(Exception ex) - { - items.Add(ex); - - return this; - } - - public void Dispose() - { - } - - public bool MoveNext(CancellationToken cancellationToken = default) - { - index++; - - return index < items.Count; - } - - public async Task MoveNextAsync(CancellationToken cancellationToken = default) - { - await Task.Delay(1, cancellationToken); - - return MoveNext(cancellationToken); - } - } - - [Fact] - public async Task Should_enumerate_over_items() - { - var result = new List(); - - var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5); - - await cursor.ForEachPipelineAsync(x => - { - result.Add(x); - return TaskHelper.Done; - }); - - Assert.Equal(new List { 0, 1, 2, 3, 4, 5 }, result); - } - - [Fact] - public async Task Should_break_when_cursor_failed() - { - var ex = new InvalidOperationException(); - - var result = new List(); - - using (var cursor = new Cursor().Add(0, 1, 2).Add(ex).Add(3, 4, 5)) - { - await Assert.ThrowsAsync(() => - { - return cursor.ForEachPipelineAsync(x => - { - result.Add(x); - return TaskHelper.Done; - }); - }); - } - - Assert.Equal(new List { 0, 1, 2 }, result); - } - - [Fact] - public async Task Should_break_when_handler_failed() - { - var ex = new InvalidOperationException(); - - var result = new List(); - - using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) - { - await Assert.ThrowsAsync(() => - { - return cursor.ForEachPipelineAsync(x => - { - if (x == 2) - { - throw ex; - } - - result.Add(x); - return TaskHelper.Done; - }); - }); - } - - Assert.Equal(new List { 0, 1 }, result); - } - - [Fact] - public async Task Should_stop_when_cancelled1() - { - using (var cts = new CancellationTokenSource()) - { - var result = new List(); - - using (var cursor = new Cursor().Add(0, 1, 2, 3, 4, 5)) - { - await Assert.ThrowsAnyAsync(() => - { - return cursor.ForEachPipelineAsync(x => - { - if (x == 2) - { - cts.Cancel(); - } - - result.Add(x); - - return TaskHelper.Done; - }, cts.Token); - }); - } - - Assert.Equal(new List { 0, 1, 2 }, result); - } - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs deleted file mode 100644 index 44a09138a..000000000 --- a/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class NamedIdTests - { - [Fact] - public void Should_instantiate_token() - { - var id = Guid.NewGuid(); - - var namedId = NamedId.Of(id, "my-name"); - - Assert.Equal(id, namedId.Id); - Assert.Equal("my-name", namedId.Name); - } - - [Fact] - public void Should_convert_named_id_to_string() - { - var id = Guid.NewGuid(); - - var namedId = NamedId.Of(id, "my-name"); - - Assert.Equal($"{id},my-name", namedId.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var named_id1_name1_a = NamedId.Of(id1, "name1"); - var named_id1_name1_b = NamedId.Of(id1, "name1"); - - var named_id2_name1 = NamedId.Of(id2, "name1"); - var named_id1_name2 = NamedId.Of(id1, "name2"); - - Assert.Equal(named_id1_name1_a, named_id1_name1_b); - Assert.Equal(named_id1_name1_a.GetHashCode(), named_id1_name1_b.GetHashCode()); - Assert.True(named_id1_name1_a.Equals((object)named_id1_name1_b)); - - Assert.NotEqual(named_id1_name1_a, named_id2_name1); - Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id2_name1.GetHashCode()); - Assert.False(named_id1_name1_a.Equals((object)named_id2_name1)); - - Assert.NotEqual(named_id1_name1_a, named_id1_name2); - Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id1_name2.GetHashCode()); - Assert.False(named_id1_name1_a.Equals((object)named_id1_name2)); - } - - [Fact] - public void Should_serialize_and_deserialize_null_guid_token() - { - NamedId value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_guid_token() - { - var value = NamedId.Of(Guid.NewGuid(), "my-name"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_null_long_token() - { - NamedId value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_long_token() - { - var value = NamedId.Of(123L, "my-name"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_null_string_token() - { - NamedId value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_string_token() - { - var value = NamedId.Of(Guid.NewGuid().ToString(), "my-name"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_throw_exception_if_string_id_is_not_valid() - { - Assert.ThrowsAny(() => JsonHelper.Deserialize>("123")); - } - - [Fact] - public void Should_throw_exception_if_long_id_is_not_valid() - { - Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-long,name")); - } - - [Fact] - public void Should_throw_exception_if_guid_id_is_not_valid() - { - Assert.ThrowsAny(() => JsonHelper.Deserialize>("invalid-guid,name")); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs deleted file mode 100644 index dc1e277a0..000000000 --- a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameIndexGrainTests - { - private readonly IGrainState> grainState = A.Fake>>(); - private readonly NamedId id1 = NamedId.Of(Guid.NewGuid(), "my-name1"); - private readonly NamedId id2 = NamedId.Of(Guid.NewGuid(), "my-name2"); - private readonly UniqueNameIndexGrain, Guid> sut; - - public UniqueNameIndexGrainTests() - { - A.CallTo(() => grainState.ClearAsync()) - .Invokes(() => grainState.Value = new UniqueNameIndexState()); - - sut = new UniqueNameIndexGrain, Guid>(grainState); - } - - [Fact] - public async Task Should_not_write_to_state_for_reservation() - { - await sut.ReserveAsync(id1.Id, id1.Name); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_to_index_if_reservation_token_acquired() - { - await AddAsync(id1); - - var result = await sut.GetIdAsync(id1.Name); - - Assert.Equal(id1.Id, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_already_reserved() - { - await sut.ReserveAsync(id1.Id, id1.Name); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.Null(newToken); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_taken() - { - await AddAsync(id1); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.Null(newToken); - } - - [Fact] - public async Task Should_provide_number_of_entries() - { - await AddAsync(id1); - await AddAsync(id2); - - var count = await sut.CountAsync(); - - Assert.Equal(2, count); - } - - [Fact] - public async Task Should_clear_all_entries() - { - await AddAsync(id1); - await AddAsync(id2); - - await sut.ClearAsync(); - - var count = await sut.CountAsync(); - - Assert.Equal(0, count); - } - - [Fact] - public async Task Should_make_reservation_after_reservation_removed() - { - var token = await sut.ReserveAsync(id1.Id, id1.Name); - - await sut.RemoveReservationAsync(token); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.NotNull(newToken); - } - - [Fact] - public async Task Should_make_reservation_after_id_removed() - { - await AddAsync(id1); - - await sut.RemoveAsync(id1.Id); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.NotNull(newToken); - } - - [Fact] - public async Task Should_remove_id_from_index() - { - await AddAsync(id1); - - await sut.RemoveAsync(id1.Id); - - var result = await sut.GetIdAsync(id1.Name); - - Assert.Equal(Guid.Empty, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_not_write_to_state_if_nothing_removed() - { - await sut.RemoveAsync(id1.Id); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_ignore_error_if_removing_reservation_with_Invalid_token() - { - await sut.RemoveReservationAsync(null); - } - - [Fact] - public async Task Should_ignore_error_if_completing_reservation_with_Invalid_token() - { - await sut.AddAsync(null); - } - - [Fact] - public async Task Should_replace_ids_on_rebuild() - { - var state = new Dictionary - { - [id1.Name] = id1.Id, - [id2.Name] = id2.Id - }; - - await sut.RebuildAsync(state); - - Assert.Equal(id1.Id, await sut.GetIdAsync(id1.Name)); - Assert.Equal(id2.Id, await sut.GetIdAsync(id2.Name)); - - var result = await sut.GetIdsAsync(); - - Assert.Equal(new List { id1.Id, id2.Id }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_provide_multiple_ids_by_names() - { - await AddAsync(id1); - await AddAsync(id2); - - var result = await sut.GetIdsAsync(new string[] { id1.Name, id2.Name, "not-found" }); - - Assert.Equal(new List { id1.Id, id2.Id }, result); - } - - private async Task AddAsync(NamedId id) - { - var token = await sut.ReserveAsync(id.Id, id.Name); - - await sut.AddAsync(token); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs deleted file mode 100644 index 11deb02cc..000000000 --- a/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// JsonExternalSerializerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.IO; -using FakeItEasy; -using Orleans.Serialization; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class JsonExternalSerializerTests - { - public JsonExternalSerializerTests() - { - J.DefaultSerializer = JsonHelper.DefaultSerializer; - } - - [Fact] - public void Should_not_copy_null() - { - var v = (string)null; - var c = J.Copy(v, null); - - Assert.Null(c); - } - - [Fact] - public void Should_copy_null_json() - { - var v = new J>(null); - var c = (J>)J.Copy(v, null); - - Assert.Null(c.Value); - } - - [Fact] - public void Should_not_copy_immutable_values() - { - var v = new List { 1, 2, 3 }.AsJ(); - var c = (J>)J.Copy(v, null); - - Assert.Same(v.Value, c.Value); - } - - [Fact] - public void Should_serialize_and_deserialize_value() - { - SerializeAndDeserialize(ArrayOfLength(100), Assert.Equal); - } - - [Fact] - public void Should_serialize_and_deserialize_large_value() - { - SerializeAndDeserialize(ArrayOfLength(8000), Assert.Equal); - } - - private static void SerializeAndDeserialize(T value, Action equals) where T : class - { - var buffer = new MemoryStream(); - - J.Serialize(J.Of(value), CreateWriter(buffer), typeof(T)); - - buffer.Position = 0; - - var copy = (J)J.Deserialize(typeof(J), CreateReader(buffer)); - - equals(copy.Value, value); - - Assert.NotSame(value, copy.Value); - } - - private static DeserializationContext CreateReader(MemoryStream buffer) - { - var reader = A.Fake(); - - A.CallTo(() => reader.ReadByteArray(A.Ignored, A.Ignored, A.Ignored)) - .Invokes(new Action((b, o, l) => buffer.Read(b, o, l))); - A.CallTo(() => reader.CurrentPosition) - .ReturnsLazily(x => (int)buffer.Position); - A.CallTo(() => reader.Length) - .ReturnsLazily(x => (int)buffer.Length); - - return new DeserializationContext(null) { StreamReader = reader }; - } - - private static SerializationContext CreateWriter(MemoryStream buffer) - { - var writer = A.Fake(); - - A.CallTo(() => writer.Write(A.Ignored, A.Ignored, A.Ignored)) - .Invokes(new Action(buffer.Write)); - A.CallTo(() => writer.CurrentOffset) - .ReturnsLazily(x => (int)buffer.Position); - - return new SerializationContext(null) { StreamWriter = writer }; - } - - private static List ArrayOfLength(int length) - { - var result = new List(); - - for (var i = 0; i < length; i++) - { - result.Add(i); - } - - return result; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs deleted file mode 100644 index ead17c4d1..000000000 --- a/tests/Squidex.Infrastructure.Tests/Orleans/LockGrainTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class LockGrainTests - { - private readonly LockGrain sut = new LockGrain(); - - [Fact] - public async Task Should_not_acquire_lock_when_locked() - { - var releaseLock1 = await sut.AcquireLockAsync("Key1"); - var releaseLock2 = await sut.AcquireLockAsync("Key1"); - - Assert.NotNull(releaseLock1); - Assert.Null(releaseLock2); - } - - [Fact] - public async Task Should_acquire_lock_with_other_key() - { - var releaseLock1 = await sut.AcquireLockAsync("Key1"); - var releaseLock2 = await sut.AcquireLockAsync("Key2"); - - Assert.NotNull(releaseLock1); - Assert.NotNull(releaseLock2); - } - - [Fact] - public async Task Should_acquire_lock_after_released() - { - var releaseLock1 = await sut.AcquireLockAsync("Key1"); - - await sut.ReleaseLockAsync(releaseLock1); - - var releaseLock2 = await sut.AcquireLockAsync("Key1"); - - Assert.NotNull(releaseLock1); - Assert.NotNull(releaseLock2); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs deleted file mode 100644 index 9cde6d797..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/JsonQueryConversionTests.cs +++ /dev/null @@ -1,382 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using NJsonSchema; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class JsonQueryConversionTests - { - private readonly List errors = new List(); - private readonly JsonSchema schema = new JsonSchema(); - - public JsonQueryConversionTests() - { - var nested = new JsonSchemaProperty { Title = "nested" }; - - nested.Properties["property"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["boolean"] = new JsonSchemaProperty - { - Type = JsonObjectType.Boolean - }; - - schema.Properties["datetime"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime - }; - - schema.Properties["guid"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.Guid - }; - - schema.Properties["integer"] = new JsonSchemaProperty - { - Type = JsonObjectType.Integer - }; - - schema.Properties["number"] = new JsonSchemaProperty - { - Type = JsonObjectType.Number - }; - - schema.Properties["string"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["stringArray"] = new JsonSchemaProperty - { - Item = new JsonSchema - { - Type = JsonObjectType.String - }, - Type = JsonObjectType.Array - }; - - schema.Properties["object"] = nested; - - schema.Properties["reference"] = new JsonSchemaProperty - { - Reference = nested - }; - } - - [Fact] - public void Should_add_error_if_property_does_not_exist() - { - var json = new { path = "notfound", op = "eq", value = 1 }; - - AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); - } - - [Fact] - public void Should_add_error_if_nested_property_does_not_exist() - { - var json = new { path = "object.notfound", op = "eq", value = 1 }; - - AssertErrors(json, "'notfound' is not a property of 'nested'."); - } - - [Theory] - [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("empty", "empty(datetime)")] - [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] - [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] - [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] - [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] - [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] - [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] - [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] - public void Should_parse_datetime_string_filter(string op, string expected) - { - var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_date_string_filter() - { - var json = new { path = "datetime", op = "eq", value = "2012-11-10" }; - - AssertFilter(json, "datetime == 2012-11-10T00:00:00Z"); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_string_value() - { - var json = new { path = "datetime", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_value() - { - var json = new { path = "datetime", op = "eq", value = 1 }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("empty", "empty(guid)")] - [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - public void Should_parse_guid_string_filter(string op, string expected) - { - var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_string_value() - { - var json = new { path = "guid", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_value() - { - var json = new { path = "guid", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(string, 'Hello')")] - [InlineData("empty", "empty(string)")] - [InlineData("endswith", "endsWith(string, 'Hello')")] - [InlineData("eq", "string == 'Hello'")] - [InlineData("ge", "string >= 'Hello'")] - [InlineData("gt", "string > 'Hello'")] - [InlineData("le", "string <= 'Hello'")] - [InlineData("lt", "string < 'Hello'")] - [InlineData("ne", "string != 'Hello'")] - [InlineData("startswith", "startsWith(string, 'Hello')")] - public void Should_parse_string_filter(string op, string expected) - { - var json = new { path = "string", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_string_property_got_invalid_value() - { - var json = new { path = "string", op = "eq", value = 1 }; - - AssertErrors(json, "Expected String for path 'string', but got Number."); - } - - [Fact] - public void Should_parse_string_in_filter() - { - var json = new { path = "string", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "string in ['Hello']"); - } - - [Fact] - public void Should_parse_nested_string_filter() - { - var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "object.property in ['Hello']"); - } - - [Fact] - public void Should_parse_referenced_string_filter() - { - var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "reference.property in ['Hello']"); - } - - [Theory] - [InlineData("eq", "number == 12")] - [InlineData("ge", "number >= 12")] - [InlineData("gt", "number > 12")] - [InlineData("le", "number <= 12")] - [InlineData("lt", "number < 12")] - [InlineData("ne", "number != 12")] - public void Should_parse_number_filter(string op, string expected) - { - var json = new { path = "number", op, value = 12 }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_number_property_got_invalid_value() - { - var json = new { path = "number", op = "eq", value = true }; - - AssertErrors(json, "Expected Number for path 'number', but got Boolean."); - } - - [Fact] - public void Should_parse_number_in_filter() - { - var json = new { path = "number", op = "in", value = new[] { 12 } }; - - AssertFilter(json, "number in [12]"); - } - - [Theory] - [InlineData("eq", "boolean == True")] - [InlineData("ne", "boolean != True")] - public void Should_parse_boolean_filter(string op, string expected) - { - var json = new { path = "boolean", op, value = true }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_boolean_property_got_invalid_value() - { - var json = new { path = "boolean", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); - } - - [Fact] - public void Should_parse_boolean_in_filter() - { - var json = new { path = "boolean", op = "in", value = new[] { true } }; - - AssertFilter(json, "boolean in [True]"); - } - - [Theory] - [InlineData("empty", "empty(stringArray)")] - [InlineData("eq", "stringArray == 'Hello'")] - [InlineData("ne", "stringArray != 'Hello'")] - public void Should_parse_array_filter(string op, string expected) - { - var json = new { path = "stringArray", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_array_in_filter() - { - var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "stringArray in ['Hello']"); - } - - [Fact] - public void Should_add_error_when_using_array_value_for_non_allowed_operator() - { - var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; - - AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); - } - - [Fact] - public void Should_parse_query() - { - var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; - - AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); - } - - [Fact] - public void Should_parse_query_with_sorting() - { - var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; - - AssertQuery(json, "Sort: string Ascending"); - } - - [Fact] - public void Should_throw_exception_for_invalid_query() - { - var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; - - Assert.Throws(() => AssertQuery(json, null)); - } - - [Fact] - public void Should_throw_exception_when_parsing_invalid_json() - { - var json = "invalid"; - - Assert.Throws(() => AssertQuery(json, null)); - } - - private void AssertQuery(object json, string expectedFilter) - { - var filter = ConvertQuery(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertFilter(object json, string expectedFilter) - { - var filter = ConvertFilter(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertErrors(object json, params string[] expectedErrors) - { - var filter = ConvertFilter(json); - - Assert.Equal(expectedErrors.ToList(), errors); - - Assert.Null(filter); - } - - private string ConvertFilter(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); - - return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); - } - - private string ConvertQuery(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); - - return jsonFilter.ToString(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs deleted file mode 100644 index 3c1b86e7f..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public class PascalCasePathConverterTests - { - [Fact] - public void Should_convert_property() - { - var source = ClrFilter.Eq("property", 1); - var result = PascalCasePathConverter.Transform(source); - - Assert.Equal("Property == 1", result.ToString()); - } - - [Fact] - public void Should_convert_properties() - { - var source = ClrFilter.Eq("root.child", 1); - var result = PascalCasePathConverter.Transform(source); - - Assert.Equal("Root.Child == 1", result.ToString()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs deleted file mode 100644 index 91566e114..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs +++ /dev/null @@ -1,374 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using NJsonSchema; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public sealed class QueryJsonConversionTests - { - private readonly List errors = new List(); - private readonly JsonSchema schema = new JsonSchema(); - - public QueryJsonConversionTests() - { - var nested = new JsonSchemaProperty { Title = "nested" }; - - nested.Properties["property"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["boolean"] = new JsonSchemaProperty - { - Type = JsonObjectType.Boolean - }; - - schema.Properties["datetime"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime - }; - - schema.Properties["guid"] = new JsonSchemaProperty - { - Type = JsonObjectType.String, Format = JsonFormatStrings.Guid - }; - - schema.Properties["integer"] = new JsonSchemaProperty - { - Type = JsonObjectType.Integer - }; - - schema.Properties["number"] = new JsonSchemaProperty - { - Type = JsonObjectType.Number - }; - - schema.Properties["string"] = new JsonSchemaProperty - { - Type = JsonObjectType.String - }; - - schema.Properties["stringArray"] = new JsonSchemaProperty - { - Item = new JsonSchema - { - Type = JsonObjectType.String - }, - Type = JsonObjectType.Array - }; - - schema.Properties["object"] = nested; - - schema.Properties["reference"] = new JsonSchemaProperty - { - Reference = nested - }; - } - - [Fact] - public void Should_add_error_if_property_does_not_exist() - { - var json = new { path = "notfound", op = "eq", value = 1 }; - - AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); - } - - [Fact] - public void Should_add_error_if_nested_property_does_not_exist() - { - var json = new { path = "object.notfound", op = "eq", value = 1 }; - - AssertErrors(json, "'notfound' is not a property of 'nested'."); - } - - [Theory] - [InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("empty", "empty(datetime)")] - [InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] - [InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] - [InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] - [InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] - [InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] - [InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] - [InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] - [InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] - public void Should_parse_datetime_string_filter(string op, string expected) - { - var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_string_value() - { - var json = new { path = "datetime", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_datetime_string_property_got_invalid_value() - { - var json = new { path = "datetime", op = "eq", value = 1 }; - - AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("empty", "empty(guid)")] - [InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - [InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] - [InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] - public void Should_parse_guid_string_filter(string op, string expected) - { - var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_string_value() - { - var json = new { path = "guid", op = "eq", value = "invalid" }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_guid_string_property_got_invalid_value() - { - var json = new { path = "guid", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); - } - - [Theory] - [InlineData("contains", "contains(string, 'Hello')")] - [InlineData("empty", "empty(string)")] - [InlineData("endswith", "endsWith(string, 'Hello')")] - [InlineData("eq", "string == 'Hello'")] - [InlineData("ge", "string >= 'Hello'")] - [InlineData("gt", "string > 'Hello'")] - [InlineData("le", "string <= 'Hello'")] - [InlineData("lt", "string < 'Hello'")] - [InlineData("ne", "string != 'Hello'")] - [InlineData("startswith", "startsWith(string, 'Hello')")] - public void Should_parse_string_filter(string op, string expected) - { - var json = new { path = "string", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_string_property_got_invalid_value() - { - var json = new { path = "string", op = "eq", value = 1 }; - - AssertErrors(json, "Expected String for path 'string', but got Number."); - } - - [Fact] - public void Should_parse_string_in_filter() - { - var json = new { path = "string", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "string in ['Hello']"); - } - - [Fact] - public void Should_parse_nested_string_filter() - { - var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "object.property in ['Hello']"); - } - - [Fact] - public void Should_parse_referenced_string_filter() - { - var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "reference.property in ['Hello']"); - } - - [Theory] - [InlineData("eq", "number == 12")] - [InlineData("ge", "number >= 12")] - [InlineData("gt", "number > 12")] - [InlineData("le", "number <= 12")] - [InlineData("lt", "number < 12")] - [InlineData("ne", "number != 12")] - public void Should_parse_number_filter(string op, string expected) - { - var json = new { path = "number", op, value = 12 }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_number_property_got_invalid_value() - { - var json = new { path = "number", op = "eq", value = true }; - - AssertErrors(json, "Expected Number for path 'number', but got Boolean."); - } - - [Fact] - public void Should_parse_number_in_filter() - { - var json = new { path = "number", op = "in", value = new[] { 12 } }; - - AssertFilter(json, "number in [12]"); - } - - [Theory] - [InlineData("eq", "boolean == True")] - [InlineData("ne", "boolean != True")] - public void Should_parse_boolean_filter(string op, string expected) - { - var json = new { path = "boolean", op, value = true }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_add_error_if_boolean_property_got_invalid_value() - { - var json = new { path = "boolean", op = "eq", value = 1 }; - - AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); - } - - [Fact] - public void Should_parse_boolean_in_filter() - { - var json = new { path = "boolean", op = "in", value = new[] { true } }; - - AssertFilter(json, "boolean in [True]"); - } - - [Theory] - [InlineData("empty", "empty(stringArray)")] - [InlineData("eq", "stringArray == 'Hello'")] - [InlineData("ne", "stringArray != 'Hello'")] - public void Should_parse_array_filter(string op, string expected) - { - var json = new { path = "stringArray", op, value = "Hello" }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_array_in_filter() - { - var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; - - AssertFilter(json, "stringArray in ['Hello']"); - } - - [Fact] - public void Should_add_error_when_using_array_value_for_non_allowed_operator() - { - var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; - - AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); - } - - [Fact] - public void Should_parse_query() - { - var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; - - AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); - } - - [Fact] - public void Should_parse_query_with_sorting() - { - var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; - - AssertQuery(json, "Sort: string Ascending"); - } - - [Fact] - public void Should_throw_exception_for_invalid_query() - { - var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; - - Assert.Throws(() => AssertQuery(json, null)); - } - - [Fact] - public void Should_throw_exception_when_parsing_invalid_json() - { - var json = "invalid"; - - Assert.Throws(() => AssertQuery(json, null)); - } - - private void AssertQuery(object json, string expectedFilter) - { - var filter = ConvertQuery(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertFilter(object json, string expectedFilter) - { - var filter = ConvertFilter(json); - - Assert.Empty(errors); - - Assert.Equal(expectedFilter, filter); - } - - private void AssertErrors(object json, params string[] expectedErrors) - { - var filter = ConvertFilter(json); - - Assert.Equal(expectedErrors.ToList(), errors); - - Assert.Null(filter); - } - - private string ConvertFilter(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = JsonHelper.DefaultSerializer.Deserialize>(json); - - return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); - } - - private string ConvertQuery(T value) - { - var json = JsonHelper.DefaultSerializer.Serialize(value, true); - - var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); - - return jsonFilter.ToString(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs deleted file mode 100644 index bdac075d4..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs +++ /dev/null @@ -1,424 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.OData.Edm; -using Squidex.Infrastructure.Queries.OData; -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public class QueryODataConversionTests - { - private static readonly IEdmModel EdmModel; - - static QueryODataConversionTests() - { - var entityType = new EdmEntityType("Squidex", "Users"); - - entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid); - entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean); - entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date); - entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double); - entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32); - - var container = new EdmEntityContainer("Squidex", "Container"); - - container.AddEntitySet("UserSet", entityType); - - var model = new EdmModel(); - - model.AddElement(container); - model.AddElement(entityType); - - EdmModel = model; - } - - [Fact] - public void Should_parse_query() - { - var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); - - Assert.NotNull(parser); - } - - [Fact] - public void Should_parse_filter_when_type_is_datetime() - { - var i = Q("$filter=created eq 1988-01-19T12:00:00Z"); - var o = C("Filter: created == 1988-01-19T12:00:00Z"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_datetime_list() - { - var i = Q("$filter=created in ('1988-01-19T12:00:00Z')"); - var o = C("Filter: created in [1988-01-19T12:00:00Z]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_date() - { - var i = Q("$filter=created eq 1988-01-19"); - var o = C("Filter: created == 1988-01-19T00:00:00Z"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_date_list() - { - var i = Q("$filter=created in ('1988-01-19')"); - var o = C("Filter: created in [1988-01-19T00:00:00Z]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_guid() - { - var i = Q("$filter=id eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); - var o = C("Filter: id == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_guid_list() - { - var i = Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); - var o = C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_null() - { - var i = Q("$filter=firstName eq null"); - var o = C("Filter: firstName == null"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_string() - { - var i = Q("$filter=firstName eq 'Dagobert'"); - var o = C("Filter: firstName == 'Dagobert'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_string_list() - { - var i = Q("$filter=firstName in ('Dagobert')"); - var o = C("Filter: firstName in ['Dagobert']"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_boolean() - { - var i = Q("$filter=isComicFigure eq true"); - var o = C("Filter: isComicFigure == True"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_boolean_list() - { - var i = Q("$filter=isComicFigure in (true)"); - var o = C("Filter: isComicFigure in [True]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int32() - { - var i = Q("$filter=age eq 60"); - var o = C("Filter: age == 60"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int32_list() - { - var i = Q("$filter=age in (60)"); - var o = C("Filter: age in [60]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int64() - { - var i = Q("$filter=incomeCents eq 31543143513456789"); - var o = C("Filter: incomeCents == 31543143513456789"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_int64_list() - { - var i = Q("$filter=incomeCents in (31543143513456789)"); - var o = C("Filter: incomeCents in [31543143513456789]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_double() - { - var i = Q("$filter=incomeMio eq 5634474356.1233"); - var o = C("Filter: incomeMio == 5634474356.1233"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_when_type_is_double_list() - { - var i = Q("$filter=incomeMio in (5634474356.1233)"); - var o = C("Filter: incomeMio in [5634474356.1233]"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_negation() - { - var i = Q("$filter=not endswith(lastName, 'Duck')"); - var o = C("Filter: !(endsWith(lastName, 'Duck'))"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_startswith() - { - var i = Q("$filter=startswith(lastName, 'Duck')"); - var o = C("Filter: startsWith(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_endswith() - { - var i = Q("$filter=endswith(lastName, 'Duck')"); - var o = C("Filter: endsWith(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_empty() - { - var i = Q("$filter=empty(lastName)"); - var o = C("Filter: empty(lastName)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_empty_to_true() - { - var i = Q("$filter=empty(lastName) eq true"); - var o = C("Filter: empty(lastName)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_contains() - { - var i = Q("$filter=contains(lastName, 'Duck')"); - var o = C("Filter: contains(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_contains_to_true() - { - var i = Q("$filter=contains(lastName, 'Duck') eq true"); - var o = C("Filter: contains(lastName, 'Duck')"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_contains_to_false() - { - var i = Q("$filter=contains(lastName, 'Duck') eq false"); - var o = C("Filter: !(contains(lastName, 'Duck'))"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_equals() - { - var i = Q("$filter=age eq 1"); - var o = C("Filter: age == 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_notequals() - { - var i = Q("$filter=age ne 1"); - var o = C("Filter: age != 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_lessthan() - { - var i = Q("$filter=age lt 1"); - var o = C("Filter: age < 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_lessthanorequal() - { - var i = Q("$filter=age le 1"); - var o = C("Filter: age <= 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_greaterthan() - { - var i = Q("$filter=age gt 1"); - var o = C("Filter: age > 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_greaterthanorequal() - { - var i = Q("$filter=age ge 1"); - var o = C("Filter: age >= 1"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_conjunction_and_contains() - { - var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); - var o = C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_conjunction() - { - var i = Q("$filter=age eq 1 and age eq 2"); - var o = C("Filter: (age == 1 && age == 2)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_disjunction() - { - var i = Q("$filter=age eq 1 or age eq 2"); - var o = C("Filter: (age == 1 || age == 2)"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_full_text_numbers() - { - var i = Q("$search=\"33k\""); - var o = C("FullText: '33k'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_full_text() - { - var i = Q("$search=Duck"); - var o = C("FullText: 'Duck'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_with_full_text_and_multiple_terms() - { - var i = Q("$search=Dagobert or Donald"); - var o = C("FullText: 'Dagobert or Donald'"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_single_field() - { - var i = Q("$orderby=age desc"); - var o = C("Sort: age Descending"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_make_orderby_with_multiple_field() - { - var i = Q("$orderby=age, incomeMio desc"); - var o = C("Sort: age Ascending, incomeMio Descending"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_parse_filter_and_take() - { - var i = Q("$top=3&$skip=4"); - var o = C("Skip: 4; Take: 3"); - - Assert.Equal(o, i); - } - - private static string C(string value) - { - return value; - } - - private static string Q(string value) - { - var parser = EdmModel.ParseQuery(value); - - return parser.ToQuery().ToString(); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs deleted file mode 100644 index 0ebd75b3a..000000000 --- a/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Xunit; - -namespace Squidex.Infrastructure.Queries -{ - public class QueryOptimizationTests - { - [Fact] - public void Should_not_convert_optimize_valid_logical_filter() - { - var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("(path == 2 || path == 3)", result.ToString()); - } - - [Fact] - public void Should_return_filter_When_logical_filter_has_one_child() - { - var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); - - var result = Optimizer.Optimize(source); - - Assert.Equal("path == 1", result.ToString()); - } - - [Fact] - public void Should_return_null_when_filters_of_logical_filter_get_optimized_away() - { - var source = ClrFilter.And(ClrFilter.And()); - - var result = Optimizer.Optimize(source); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_logical_filter_has_no_filter() - { - var source = ClrFilter.And(); - - var result = Optimizer.Optimize(source); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_filter_of_negation_get_optimized_away() - { - var source = ClrFilter.Not(ClrFilter.And()); - - var result = Optimizer.Optimize(source); - - Assert.Null(result); - } - - [Fact] - public void Should_invert_equals_not_filter() - { - var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("path != 1", result.ToString()); - } - - [Fact] - public void Should_invert_notequals_not_filter() - { - var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("path == 1", result.ToString()); - } - - [Fact] - public void Should_not_convert_number_operator() - { - var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); - - var result = Optimizer.Optimize(source); - - Assert.Equal("!(path < 1)", result.ToString()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs deleted file mode 100644 index 355aba360..000000000 --- a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class RefTokenTests - { - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(":")] - [InlineData("user")] - public void Should_throw_exception_if_parsing_invalid_input(string input) - { - Assert.Throws(() => RefToken.Parse(input)); - } - - [Fact] - public void Should_instantiate_token() - { - var token = new RefToken("client", "client1"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1", token.Identifier); - - Assert.True(token.IsClient); - } - - [Fact] - public void Should_instantiate_subject_token() - { - var token = new RefToken("subject", "client1"); - - Assert.True(token.IsSubject); - } - - [Fact] - public void Should_instantiate_token_and_lower_type() - { - var token = new RefToken("Client", "client1"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1", token.Identifier); - } - - [Fact] - public void Should_parse_user_token_from_string() - { - var token = RefToken.Parse("client:client1"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1", token.Identifier); - } - - [Fact] - public void Should_parse_user_token_with_colon_in_identifier() - { - var token = RefToken.Parse("client:client1:app"); - - Assert.Equal("client", token.Type); - Assert.Equal("client1:app", token.Identifier); - } - - [Fact] - public void Should_convert_user_token_to_string() - { - var token = RefToken.Parse("client:client1"); - - Assert.Equal("client:client1", token.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var token_type1_id1_a = RefToken.Parse("type1:client1"); - var token_type1_id1_b = RefToken.Parse("type1:client1"); - - var token_type2_id1 = RefToken.Parse("type2:client1"); - var token_type1_id2 = RefToken.Parse("type1:client2"); - - Assert.Equal(token_type1_id1_a, token_type1_id1_b); - Assert.Equal(token_type1_id1_a.GetHashCode(), token_type1_id1_b.GetHashCode()); - Assert.True(token_type1_id1_a.Equals((object)token_type1_id1_b)); - - Assert.NotEqual(token_type1_id1_a, token_type2_id1); - Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type2_id1.GetHashCode()); - Assert.False(token_type1_id1_a.Equals((object)token_type2_id1)); - - Assert.NotEqual(token_type1_id1_a, token_type1_id2); - Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type1_id2.GetHashCode()); - Assert.False(token_type1_id1_a.Equals((object)token_type1_id2)); - } - - [Fact] - public void Should_serialize_and_deserialize_null_token() - { - RefToken value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_token() - { - var value = RefToken.Parse("client:client1"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs deleted file mode 100644 index 36b770438..000000000 --- a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Diagnostics; -using Xunit; - -namespace Squidex.Infrastructure.Reflection -{ - public class SimpleMapperTests - { - public class Class1Base - { - public T1 P1 { get; set; } - } - - public class Class1 : Class1Base - { - public T2 P2 { get; set; } - } - - public class Class2Base - { - public T2 P2 { get; set; } - } - - public class Class2 : Class2Base - { - public T3 P3 { get; set; } - } - - public class Readonly - { - public T P1 { get; } - } - - public class Writeonly - { - public T P1 - { - set { Debug.WriteLine(value); } - } - } - - [Fact] - public void Should_throw_exception_if_mapping_with_null_source() - { - Assert.Throws(() => SimpleMapper.Map((Class2)null, new Class2())); - } - - [Fact] - public void Should_throw_exception_if_mapping_with_null_target() - { - Assert.Throws(() => SimpleMapper.Map(new Class2(), (Class2)null)); - } - - [Fact] - public void Should_map_to_same_type() - { - var obj1 = new Class1 - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } - - [Fact] - public void Should_map_all_properties() - { - var obj1 = new Class1 - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class1()); - - Assert.Equal(6, obj2.P1); - Assert.Equal(8, obj2.P2); - } - - [Fact] - public void Should_map_to_convertible_type() - { - var obj1 = new Class1 - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } - - [Fact] - public void Should_map_nullables() - { - var obj1 = new Class1 - { - P1 = true, - P2 = true - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.True(obj2.P2); - Assert.False(obj2.P3); - } - - [Fact] - public void Should_map_when_convertible_is_null() - { - var obj1 = new Class1 - { - P1 = null, - P2 = null - }; - var obj2 = SimpleMapper.Map(obj1, new Class1()); - - Assert.Equal(0, obj2.P1); - Assert.Equal(0, obj2.P2); - } - - [Fact] - public void Should_convert_to_string() - { - var obj1 = new Class1 - { - P1 = new RefToken("user", "1"), - P2 = new RefToken("user", "2") - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal("user:2", obj2.P2); - Assert.Null(obj2.P3); - } - - [Fact] - public void Should_return_default_if_conversion_failed() - { - var obj1 = new Class1 - { - P1 = long.MaxValue, - P2 = long.MaxValue - }; - var obj2 = SimpleMapper.Map(obj1, new Class2()); - - Assert.Equal(0, obj2.P2); - Assert.Equal(0, obj2.P3); - } - - [Fact] - public void Should_ignore_write_only() - { - var obj1 = new Writeonly(); - var obj2 = SimpleMapper.Map(obj1, new Class1()); - - Assert.Equal(0, obj2.P1); - } - - [Fact] - public void Should_ignore_read_only() - { - var obj1 = new Class1 { P1 = 10 }; - var obj2 = SimpleMapper.Map(obj1, new Readonly()); - - Assert.Equal(0, obj2.P1); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs deleted file mode 100644 index 39e46480f..000000000 --- a/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using Xunit; - -namespace Squidex.Infrastructure.Security -{ - public class ExtensionsTests - { - [Fact] - public void Should_retrieve_subject() - { - TestClaimExtension(OpenIdClaims.Subject, x => x.OpenIdSubject()); - } - - [Fact] - public void Should_retrieve_client_id() - { - TestClaimExtension(OpenIdClaims.ClientId, x => x.OpenIdClientId()); - } - - [Fact] - public void Should_retrieve_preferred_user_name() - { - TestClaimExtension(OpenIdClaims.PreferredUserName, x => x.OpenIdPreferredUserName()); - } - - [Fact] - public void Should_retrieve_name() - { - TestClaimExtension(OpenIdClaims.Name, x => x.OpenIdName()); - } - - [Fact] - public void Should_retrieve_nickname() - { - TestClaimExtension(OpenIdClaims.NickName, x => x.OpenIdNickName()); - } - - [Fact] - public void Should_retrieve_email() - { - TestClaimExtension(OpenIdClaims.Email, x => x.OpenIdEmail()); - } - - private static void TestClaimExtension(string claimType, Func getter) - { - var claimValue = Guid.NewGuid().ToString(); - - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(); - - claimsIdentity.AddClaim(new Claim(claimType, claimValue)); - - Assert.Null(getter(claimsPrincipal)); - - claimsPrincipal.AddIdentity(claimsIdentity); - - Assert.Equal(claimValue, getter(claimsPrincipal)); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj deleted file mode 100644 index f0b9728a3..000000000 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Infrastructure - 7.3 - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - - - - - \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs b/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs deleted file mode 100644 index e36d802dc..000000000 --- a/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.States -{ - public class InconsistentStateExceptionTests - { - [Fact] - public void Should_serialize_and_deserialize() - { - var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); - var result = source.SerializeAndDeserializeBinary(); - - Assert.IsType(result.InnerException); - Assert.Equal("Inner", result.InnerException.Message); - - Assert.Equal(result.ExpectedVersion, source.ExpectedVersion); - Assert.Equal(result.CurrentVersion, source.CurrentVersion); - - Assert.Equal(result.Message, source.Message); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs deleted file mode 100644 index 9ebc5904b..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; -using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.TestHelpers -{ - public static class JsonHelper - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - - public static IJsonSerializer CreateSerializer(TypeNameRegistry typeNameRegistry = null) - { - var serializerSettings = DefaultSettings(typeNameRegistry); - - return new NewtonsoftJsonSerializer(serializerSettings); - } - - public static JsonSerializerSettings DefaultSettings(TypeNameRegistry typeNameRegistry = null) - { - return new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), - - ContractResolver = new ConverterContractResolver( - new ClaimsPrincipalConverter(), - new InstantConverter(), - new EnvelopeHeadersConverter(), - new FilterConverter(), - new JsonValueConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertyPathConverter(), - new RefTokenConverter(), - new StringEnumConverter()), - - TypeNameHandling = TypeNameHandling.Auto - }; - } - - public static T SerializeAndDeserialize(this T value) - { - return DefaultSerializer.Deserialize>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; - } - - public static T Deserialize(string value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; - } - - public static T Deserialize(object value) - { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs deleted file mode 100644 index 78c6a3ddb..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.TestHelpers -{ - public sealed class MyDomainObject : DomainObjectGrain - { - public MyDomainObject(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - switch (command) - { - case CreateAuto createAuto: - return Create(createAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case CreateCustom createCustom: - return CreateReturn(createCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "CREATED"; - }); - - case UpdateAuto updateAuto: - return Update(updateAuto, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - }); - - case UpdateCustom updateCustom: - return UpdateReturn(updateCustom, c => - { - RaiseEvent(new ValueChanged { Value = c.Value }); - - return "UPDATED"; - }); - } - - return Task.FromResult(null); - } - } - - public sealed class CreateAuto : MyCommand - { - public int Value { get; set; } - } - - public sealed class CreateCustom : MyCommand - { - public int Value { get; set; } - } - - public sealed class UpdateAuto : MyCommand - { - public int Value { get; set; } - } - - public sealed class UpdateCustom : MyCommand - { - public int Value { get; set; } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs deleted file mode 100644 index 6f9c0717c..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.TestHelpers -{ - public class MyGrain : DomainObjectGrain - { - public MyGrain(IStore store) - : base(store, A.Dummy()) - { - } - - protected override Task ExecuteAsync(IAggregateCommand command) - { - return Task.FromResult(null); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs deleted file mode 100644 index 82b7bced7..000000000 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ /dev/null @@ -1,228 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Infrastructure.Log; -using Xunit; - -namespace Squidex.Infrastructure.UsageTracking -{ - public class BackgroundUsageTrackerTests - { - private readonly IUsageRepository usageStore = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly string key = Guid.NewGuid().ToString(); - private readonly BackgroundUsageTracker sut; - - public BackgroundUsageTrackerTests() - { - sut = new BackgroundUsageTracker(usageStore, log); - } - - [Fact] - public async Task Should_throw_exception_if_tracking_on_disposed_object() - { - sut.Dispose(); - - await Assert.ThrowsAsync(() => sut.TrackAsync(key, "category1", 1, 1000)); - } - - [Fact] - public async Task Should_throw_exception_if_querying_on_disposed_object() - { - sut.Dispose(); - - await Assert.ThrowsAsync(() => sut.QueryAsync(key, DateTime.Today, DateTime.Today.AddDays(1))); - } - - [Fact] - public async Task Should_throw_exception_if_querying_montly_usage_on_disposed_object() - { - sut.Dispose(); - - await Assert.ThrowsAsync(() => sut.GetMonthlyCallsAsync(key, DateTime.Today)); - } - - [Fact] - public async Task Should_sum_up_when_getting_monthly_calls() - { - var date = new DateTime(2016, 1, 15); - - IReadOnlyList originalData = new List - { - new StoredUsage("category1", date.AddDays(1), Counters(10, 15)), - new StoredUsage("category1", date.AddDays(3), Counters(13, 18)), - new StoredUsage("category1", date.AddDays(5), Counters(15, 20)), - new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) - }; - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 15))) - .Returns(originalData); - - var result = await sut.GetMonthlyCallsAsync(key, date); - - Assert.Equal(55, result); - } - - [Fact] - public async Task Should_sum_up_when_getting_last_calls_calls() - { - var f = DateTime.Today; - var t = DateTime.Today.AddDays(10); - - IReadOnlyList originalData = new List - { - new StoredUsage("category1", f.AddDays(1), Counters(10, 15)), - new StoredUsage("category1", f.AddDays(3), Counters(13, 18)), - new StoredUsage("category1", f.AddDays(5), Counters(15, 20)), - new StoredUsage("category1", f.AddDays(7), Counters(17, 22)) - }; - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) - .Returns(originalData); - - var result = await sut.GetPreviousCallsAsync(key, f, t); - - Assert.Equal(55, result); - } - - [Fact] - public async Task Should_fill_missing_days() - { - var f = DateTime.Today; - var t = DateTime.Today.AddDays(4); - - var originalData = new List - { - new StoredUsage("MyCategory1", f.AddDays(1), Counters(10, 15)), - new StoredUsage("MyCategory1", f.AddDays(3), Counters(13, 18)), - new StoredUsage("MyCategory1", f.AddDays(4), Counters(15, 20)), - new StoredUsage(null, f.AddDays(0), Counters(17, 22)), - new StoredUsage(null, f.AddDays(2), Counters(11, 14)) - }; - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) - .Returns(originalData); - - var result = await sut.QueryAsync(key, f, t); - - var expected = new Dictionary> - { - ["MyCategory1"] = new List - { - new DateUsage(f.AddDays(0), 00, 00), - new DateUsage(f.AddDays(1), 10, 15), - new DateUsage(f.AddDays(2), 00, 00), - new DateUsage(f.AddDays(3), 13, 18), - new DateUsage(f.AddDays(4), 15, 20) - }, - ["*"] = new List - { - new DateUsage(f.AddDays(0), 17, 22), - new DateUsage(f.AddDays(1), 00, 00), - new DateUsage(f.AddDays(2), 11, 14), - new DateUsage(f.AddDays(3), 00, 00), - new DateUsage(f.AddDays(4), 00, 00) - } - }; - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_fill_missing_days_with_star() - { - var f = DateTime.Today; - var t = DateTime.Today.AddDays(4); - - A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) - .Returns(new List()); - - var result = await sut.QueryAsync(key, f, t); - - var expected = new Dictionary> - { - ["*"] = new List - { - new DateUsage(f.AddDays(0), 00, 00), - new DateUsage(f.AddDays(1), 00, 00), - new DateUsage(f.AddDays(2), 00, 00), - new DateUsage(f.AddDays(3), 00, 00), - new DateUsage(f.AddDays(4), 00, 00) - } - }; - - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task Should_not_track_if_weight_less_than_zero() - { - await sut.TrackAsync(key, "MyCategory", -1, 1000); - await sut.TrackAsync(key, "MyCategory", 0, 1000); - - sut.Next(); - sut.Dispose(); - - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_aggregate_and_store_on_dispose() - { - var key1 = Guid.NewGuid().ToString(); - var key2 = Guid.NewGuid().ToString(); - var key3 = Guid.NewGuid().ToString(); - - var today = DateTime.Today; - - await sut.TrackAsync(key1, "MyCategory1", 1, 1000); - - await sut.TrackAsync(key2, "MyCategory1", 1.0, 2000); - await sut.TrackAsync(key2, "MyCategory1", 0.5, 3000); - - await sut.TrackAsync(key3, "MyCategory1", 0.3, 4000); - await sut.TrackAsync(key3, "MyCategory1", 0.1, 5000); - - await sut.TrackAsync(key3, null, 0.5, 2000); - await sut.TrackAsync(key3, null, 0.5, 6000); - - UsageUpdate[] updates = null; - - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) - .Invokes((UsageUpdate[] u) => updates = u); - - sut.Next(); - sut.Dispose(); - - updates.Should().BeEquivalentTo(new[] - { - new UsageUpdate(today, $"{key1}_API", "MyCategory1", Counters(1.0, 1000)), - new UsageUpdate(today, $"{key2}_API", "MyCategory1", Counters(1.5, 5000)), - new UsageUpdate(today, $"{key3}_API", "MyCategory1", Counters(0.4, 9000)), - new UsageUpdate(today, $"{key3}_API", "*", Counters(1, 8000)) - }, o => o.ComparingByMembers()); - - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) - .MustHaveHappened(); - } - - private static Counters Counters(double count, long ms) - { - return new Counters - { - [BackgroundUsageTracker.CounterTotalCalls] = count, - [BackgroundUsageTracker.CounterTotalElapsedMs] = ms - }; - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs deleted file mode 100644 index f8c5ea381..000000000 --- a/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FluentAssertions; -using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure -{ - public class ValidationExceptionTests - { - [Fact] - public void Should_format_message_from_summary() - { - var ex = new ValidationException("Summary."); - - Assert.Equal("Summary.", ex.Message); - } - - [Fact] - public void Should_append_dot_to_summary() - { - var ex = new ValidationException("Summary"); - - Assert.Equal("Summary.", ex.Message); - } - - [Fact] - public void Should_format_message_from_errors() - { - var ex = new ValidationException("Summary", new ValidationError("Error1."), new ValidationError("Error2.")); - - Assert.Equal("Summary: Error1. Error2.", ex.Message); - } - - [Fact] - public void Should_not_add_colon_twice() - { - var ex = new ValidationException("Summary:", new ValidationError("Error1."), new ValidationError("Error2.")); - - Assert.Equal("Summary: Error1. Error2.", ex.Message); - } - - [Fact] - public void Should_append_dots_to_errors() - { - var ex = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); - - Assert.Equal("Summary: Error1. Error2.", ex.Message); - } - - [Fact] - public void Should_serialize_and_deserialize1() - { - var source = new ValidationException("Summary", new ValidationError("Error1"), null); - var result = source.SerializeAndDeserializeBinary(); - - result.Errors.Should().BeEquivalentTo(source.Errors); - - Assert.Equal(source.Message, result.Message); - Assert.Equal(source.Summary, result.Summary); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var source = new ValidationException("Summary", new ValidationError("Error1"), new ValidationError("Error2")); - var result = source.SerializeAndDeserializeBinary(); - - result.Errors.Should().BeEquivalentTo(source.Errors); - - Assert.Equal(source.Message, result.Message); - Assert.Equal(source.Summary, result.Summary); - } - } -} diff --git a/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs deleted file mode 100644 index 8f2721017..000000000 --- a/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Web -{ - public class ApiExceptionFilterAttributeTests - { - private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); - - [Fact] - public void Should_generate_404_for_DomainObjectNotFoundException() - { - var context = E(new DomainObjectNotFoundException("1", typeof(object))); - - sut.OnException(context); - - Assert.IsType(context.Result); - } - - [Fact] - public void Should_generate_400_for_ValidationException() - { - var ex = new ValidationException("NotAllowed", - new ValidationError("Error1"), - new ValidationError("Error2", "P"), - new ValidationError("Error3", "P1", "P2")); - - var context = E(ex); - - sut.OnException(context); - - var result = context.Result as ObjectResult; - - Assert.Equal(400, result.StatusCode); - Assert.Equal(400, (result.Value as ErrorDto)?.StatusCode); - - Assert.Equal(ex.Summary, (result.Value as ErrorDto).Message); - - Assert.Equal(new[] { "Error1", "P: Error2", "P1, P2: Error3" }, (result.Value as ErrorDto).Details); - } - - [Fact] - public void Should_generate_400_for_DomainException() - { - var context = E(new DomainException("NotAllowed")); - - sut.OnException(context); - - Validate(400, context); - } - - [Fact] - public void Should_generate_412_for_DomainObjectVersionException() - { - var context = E(new DomainObjectVersionException("1", typeof(object), 1, 2)); - - sut.OnException(context); - - Validate(412, context); - } - - [Fact] - public void Should_generate_403_for_DomainForbiddenException() - { - var context = E(new DomainForbiddenException("Forbidden")); - - sut.OnException(context); - - Validate(403, context); - } - - [Fact] - public void Should_generate_403_for_SecurityException() - { - var context = E(new SecurityException("Forbidden")); - - sut.OnException(context); - - Validate(403, context); - } - - private static ExceptionContext E(Exception exception) - { - var httpContext = new DefaultHttpContext(); - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List() - }); - - return new ExceptionContext(actionContext, new List()) - { - Exception = exception - }; - } - - private static void Validate(int statusCode, ExceptionContext context) - { - var result = context.Result as ObjectResult; - - Assert.Equal(statusCode, result.StatusCode); - Assert.Equal(statusCode, (result.Value as ErrorDto)?.StatusCode); - - Assert.Equal(context.Exception.Message, (result.Value as ErrorDto).Message); - } - } -} diff --git a/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs b/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs deleted file mode 100644 index 9b95902d9..000000000 --- a/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Squidex.Shared; -using Squidex.Shared.Identity; -using Xunit; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Web -{ - public class ApiPermissionAttributeTests - { - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity user = new ClaimsIdentity(); - private bool isNextCalled; - - public ApiPermissionAttributeTests() - { - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List() - }); - - actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); - - next = () => - { - isNextCalled = true; - - return Task.FromResult(null); - }; - } - - [Fact] - public void Should_use_bearer_schemes() - { - var sut = new ApiPermissionAttribute(); - - Assert.Equal("Bearer", sut.AuthenticationSchemes); - } - - [Fact] - public async Task Should_call_next_when_user_has_correct_permission() - { - actionExecutingContext.RouteData.Values["app"] = "my-app"; - - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Null(actionExecutingContext.Result); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_return_forbidden_when_user_has_wrong_permission() - { - actionExecutingContext.RouteData.Values["app"] = "my-app"; - - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_return_forbidden_when_route_data_has_no_value() - { - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_return_forbidden_when_user_has_no_permission() - { - var sut = new ApiPermissionAttribute(Permissions.AppSchemasCreate); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs deleted file mode 100644 index 094055cc6..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class ETagCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ETagCommandMiddleware sut; - - public ETagCommandMiddlewareTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); - - sut = new ETagCommandMiddleware(httpContextAccessor); - } - - [Fact] - public async Task Should_do_nothing_when_context_is_null() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(null); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Null(command.Actor); - } - - [Fact] - public async Task Should_do_nothing_if_command_has_etag_defined() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; - - var command = new CreateContent { ExpectedVersion = 1 }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(1, context.Command.ExpectedVersion); - } - - [Fact] - public async Task Should_add_expected_version_to_command() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(13, context.Command.ExpectedVersion); - } - - [Fact] - public async Task Should_add_weak_etag_as_expected_version_to_command() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(13, context.Command.ExpectedVersion); - } - - [Fact] - public async Task Should_add_version_from_result_as_etag_to_response() - { - var command = new CreateContent(); - var context = Ctx(command); - - context.Complete(new EntitySavedResult(17)); - - await sut.HandleAsync(context); - - Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_add_version_from_entity_as_etag_to_response() - { - var command = new CreateContent(); - var context = Ctx(command); - - context.Complete(new ContentEntity { Version = 17 }); - - await sut.HandleAsync(context); - - Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs deleted file mode 100644 index fb1ab5baa..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Security; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithActorCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly EnrichWithActorCommandMiddleware sut; - - public EnrichWithActorCommandMiddlewareTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); - - sut = new EnrichWithActorCommandMiddleware(httpContextAccessor); - } - - [Fact] - public async Task Should_throw_security_exception_when_no_subject_or_client_is_found() - { - var command = new CreateContent(); - var context = Ctx(command); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - - [Fact] - public async Task Should_do_nothing_when_context_is_null() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(null); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Null(command.Actor); - } - - [Fact] - public async Task Should_assign_actor_from_subject() - { - httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "me"); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(new RefToken(RefTokenType.Subject, "me"), command.Actor); - } - - [Fact] - public async Task Should_assign_actor_from_client() - { - httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); - - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(new RefToken(RefTokenType.Client, "my-client"), command.Actor); - } - - [Fact] - public async Task Should_not_override_actor() - { - httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client"); - - var command = new CreateContent { Actor = new RefToken("subject", "me") }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(new RefToken("subject", "me"), command.Actor); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - - private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue) - { - var claimsPrincipal = new ClaimsPrincipal(); - var claimsIdentity = new ClaimsIdentity(); - - claimsIdentity.AddClaim(new Claim(claimType, claimValue)); - claimsPrincipal.AddIdentity(claimsIdentity); - - return claimsPrincipal; - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs deleted file mode 100644 index 7aa15b855..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithAppIdCommandMiddlewareTests - { - private readonly IContextProvider contextProvider = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly Context requestContext = Context.Anonymous(); - private readonly EnrichWithAppIdCommandMiddleware sut; - - public EnrichWithAppIdCommandMiddlewareTests() - { - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); - - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - - requestContext.App = app; - - sut = new EnrichWithAppIdCommandMiddleware(contextProvider); - } - - [Fact] - public async Task Should_throw_exception_if_app_not_found() - { - requestContext.App = null; - - var command = new CreateContent(); - var context = Ctx(command); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - - [Fact] - public async Task Should_assign_app_id_and_name_to_app_command() - { - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(appId, command.AppId); - } - - [Fact] - public async Task Should_assign_app_id_to_app_self_command() - { - var command = new ChangePlan(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(appId.Id, command.AppId); - } - - [Fact] - public async Task Should_not_override_app_id() - { - var command = new ChangePlan { AppId = Guid.NewGuid() }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(appId.Id, command.AppId); - } - - [Fact] - public async Task Should_not_override_app_id_and_name() - { - var command = new CreateContent { AppId = NamedId.Of(Guid.NewGuid(), "other-app") }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(appId, command.AppId); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - } -} diff --git a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs deleted file mode 100644 index 5ebe6fe24..000000000 --- a/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithSchemaIdCommandMiddlewareTests - { - private readonly IActionContextAccessor actionContextAccessor = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext = new ActionContext(); - private readonly EnrichWithSchemaIdCommandMiddleware sut; - - public EnrichWithSchemaIdCommandMiddlewareTests() - { - actionContext.RouteData = new RouteData(); - actionContext.HttpContext = httpContext; - - A.CallTo(() => actionContextAccessor.ActionContext) - .Returns(actionContext); - - var app = A.Fake(); - - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - - httpContext.Context().App = app; - - var schema = A.Fake(); - - A.CallTo(() => schema.Id).Returns(schemaId.Id); - A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name)) - .Returns(schema); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) - .Returns(schema); - - sut = new EnrichWithSchemaIdCommandMiddleware(appProvider, actionContextAccessor); - } - - [Fact] - public async Task Should_throw_exception_if_schema_not_found() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, "other-schema")) - .Returns(Task.FromResult(null)); - - actionContext.RouteData.Values["name"] = "other-schema"; - - var command = new CreateContent { AppId = appId }; - var context = Ctx(command); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - } - - [Fact] - public async Task Should_do_nothing_when_route_has_no_parameter() - { - var command = new CreateContent(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Null(command.Actor); - } - - [Fact] - public async Task Should_assign_schema_id_and_name_from_name() - { - actionContext.RouteData.Values["name"] = schemaId.Name; - - var command = new CreateContent { AppId = appId }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(schemaId, command.SchemaId); - } - - [Fact] - public async Task Should_assign_schema_id_and_name_from_id() - { - actionContext.RouteData.Values["name"] = schemaId.Id; - - var command = new CreateContent { AppId = appId }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(schemaId, command.SchemaId); - } - - [Fact] - public async Task Should_assign_schema_id_from_id() - { - actionContext.RouteData.Values["name"] = schemaId.Name; - - var command = new UpdateSchema(); - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.Equal(schemaId.Id, command.SchemaId); - } - - [Fact] - public async Task Should_not_override_schema_id() - { - var command = new CreateSchema { SchemaId = Guid.NewGuid() }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(schemaId.Id, command.SchemaId); - } - - [Fact] - public async Task Should_not_override_schema_id_and_name() - { - var command = new CreateContent { SchemaId = NamedId.Of(Guid.NewGuid(), "other-schema") }; - var context = Ctx(command); - - await sut.HandleAsync(context); - - Assert.NotEqual(appId, command.AppId); - } - - private CommandContext Ctx(ICommand command) - { - return new CommandContext(command, commandBus); - } - } -} diff --git a/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs deleted file mode 100644 index eb0ddb4eb..000000000 --- a/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// 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.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure.UsageTracking; -using Xunit; - -namespace Squidex.Web.Pipeline -{ - public class ApiCostsFilterTests - { - private readonly IActionContextAccessor actionContextAccessor = A.Fake(); - private readonly IAppEntity appEntity = A.Fake(); - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IUsageTracker usageTracker = A.Fake(); - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly ActionExecutingContext actionContext; - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutionDelegate next; - private readonly ApiCostsFilter sut; - private long apiCallsMax; - private long apiCallsCurrent; - private bool isNextCalled; - - public ApiCostsFilterTests() - { - actionContext = - new ActionExecutingContext( - new ActionContext(httpContext, new RouteData(), - new ActionDescriptor()), - new List(), new Dictionary(), null); - - A.CallTo(() => actionContextAccessor.ActionContext) - .Returns(actionContext); - - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(appPlan); - - A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity)) - .Returns(appPlan); - - A.CallTo(() => appPlan.MaxApiCalls) - .ReturnsLazily(x => apiCallsMax); - - A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A.Ignored, DateTime.Today)) - .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); - - next = () => - { - isNextCalled = true; - - return Task.FromResult(null); - }; - - sut = new ApiCostsFilter(appPlansProvider, usageTracker); - } - - [Fact] - public async Task Should_return_429_status_code_if_max_calls_over_limit() - { - sut.FilterDefinition = new ApiCostsAttribute(1); - - SetupApp(); - - apiCallsCurrent = 1000; - apiCallsMax = 600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.Equal(429, (actionContext.Result as StatusCodeResult).StatusCode); - Assert.False(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_track_if_calls_left() - { - sut.FilterDefinition = new ApiCostsAttribute(13); - - SetupApp(); - - apiCallsCurrent = 1000; - apiCallsMax = 1600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_allow_small_buffer() - { - sut.FilterDefinition = new ApiCostsAttribute(13); - - SetupApp(); - - apiCallsCurrent = 1099; - apiCallsMax = 1000; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_track_if_weight_is_zero() - { - sut.FilterDefinition = new ApiCostsAttribute(0); - - SetupApp(); - - apiCallsCurrent = 1000; - apiCallsMax = 600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_track_if_app_not_defined() - { - sut.FilterDefinition = new ApiCostsAttribute(1); - - apiCallsCurrent = 1000; - apiCallsMax = 600; - - await sut.OnActionExecutionAsync(actionContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - private void SetupApp() - { - httpContext.Context().App = appEntity; - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs deleted file mode 100644 index a27aebb0a..000000000 --- a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; -using Xunit; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Web.Pipeline -{ - public class AppResolverTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext; - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity user = new ClaimsIdentity(); - private readonly string appName = "my-app"; - private readonly AppResolver sut; - private bool isNextCalled; - - public AppResolverTests() - { - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List() - }); - - actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); - actionExecutingContext.RouteData.Values["app"] = appName; - - next = () => - { - isNextCalled = true; - - return Task.FromResult(null); - }; - - sut = new AppResolver(appProvider); - } - - [Fact] - public async Task Should_return_not_found_if_app_not_found() - { - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(Task.FromResult(null)); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.IsType(actionExecutingContext.Result); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_resolve_app_from_user() - { - var app = CreateApp(appName, appUser: "user1"); - - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_resolve_app_from_client() - { - var app = CreateApp(appName, appClient: "client1"); - - user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_resolve_app_if_anonymouse_but_not_permissions() - { - var app = CreateApp(appName); - - user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - actionContext.ActionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new AllowAnonymousFilter(), 1)); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.Same(app, httpContext.Context().App); - Assert.Equal(2, user.Claims.Count()); - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_return_not_found_if_user_has_no_permissions() - { - var app = CreateApp(appName); - - user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - - A.CallTo(() => appProvider.GetAppAsync(appName)) - .Returns(app); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.IsType(actionExecutingContext.Result); - Assert.False(isNextCalled); - } - - [Fact] - public async Task Should_do_nothing_if_parameter_not_set() - { - actionExecutingContext.RouteData.Values.Remove("app"); - - await sut.OnActionExecutionAsync(actionExecutingContext, next); - - Assert.True(isNextCalled); - - A.CallTo(() => appProvider.GetAppAsync(A.Ignored)) - .MustNotHaveHappened(); - } - - private static IAppEntity CreateApp(string name, string appUser = null, string appClient = null) - { - var appEntity = A.Fake(); - - if (appUser != null) - { - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty.Assign(appUser, Role.Owner)); - } - else - { - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty); - } - - if (appClient != null) - { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty.Add(appClient, "secret")); - } - else - { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty); - } - - A.CallTo(() => appEntity.Name) - .Returns(name); - - A.CallTo(() => appEntity.Roles) - .Returns(Roles.Empty); - - return appEntity; - } - } -} diff --git a/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs b/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs deleted file mode 100644 index db46eba5c..000000000 --- a/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Squidex.Web.Pipeline -{ - public class ETagFilterTests - { - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext executingContext; - private readonly ActionExecutedContext executedContext; - private readonly ETagFilter sut = new ETagFilter(Options.Create(new ETagOptions())); - - public ETagFilterTests() - { - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - var filters = new List(); - - executingContext = new ActionExecutingContext(actionContext, filters, new Dictionary(), this); - executedContext = new ActionExecutedContext(actionContext, filters, this) - { - Result = new OkResult() - }; - } - - [Fact] - public async Task Should_not_convert_already_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_convert_strong_to_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_not_convert_empty_strong_to_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Null((string)httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_return_304_for_same_etags() - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; - - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(304, (executedContext.Result as StatusCodeResult).StatusCode); - } - - [Fact] - public async Task Should_not_return_304_for_different_etags() - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11"; - - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(200, (executedContext.Result as StatusCodeResult).StatusCode); - } - - private ActionExecutionDelegate Next() - { - return () => Task.FromResult(executedContext); - } - } -} diff --git a/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj deleted file mode 100644 index f4a516ddb..000000000 --- a/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - Squidex.Web - 7.3 - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/GenerateLanguages/GenerateLanguages.csproj b/tools/GenerateLanguages/GenerateLanguages.csproj deleted file mode 100644 index 6a6e46772..000000000 --- a/tools/GenerateLanguages/GenerateLanguages.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - netcoreapp2.2 - Exe - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/LoadTest/LoadTest.csproj b/tools/LoadTest/LoadTest.csproj deleted file mode 100644 index 5a97587d8..000000000 --- a/tools/LoadTest/LoadTest.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - Exe - netcoreapp2.2 - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/Migrate_00/Migrate_00.csproj b/tools/Migrate_00/Migrate_00.csproj deleted file mode 100644 index de3cd40b1..000000000 --- a/tools/Migrate_00/Migrate_00.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - Exe - netcoreapp2.2 - 2.2.0 - 7.3 - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/Migrate_01/Migrate_01.csproj b/tools/Migrate_01/Migrate_01.csproj deleted file mode 100644 index c2bda99af..000000000 --- a/tools/Migrate_01/Migrate_01.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - netstandard2.0 - 7.3 - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - - - - diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs deleted file mode 100644 index 6070a13a9..000000000 --- a/tools/Migrate_01/MigrationPath.cs +++ /dev/null @@ -1,132 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Migrate_01.Migrations; -using Migrate_01.Migrations.MongoDb; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01 -{ - public sealed class MigrationPath : IMigrationPath - { - private const int CurrentVersion = 19; - private readonly IServiceProvider serviceProvider; - - public MigrationPath(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } - - public (int Version, IEnumerable Migrations) GetNext(int version) - { - if (version == CurrentVersion) - { - return (CurrentVersion, null); - } - - var migrations = ResolveMigrators(version).Where(x => x != null).ToList(); - - return (CurrentVersion, migrations); - } - - private IEnumerable ResolveMigrators(int version) - { - yield return serviceProvider.GetRequiredService(); - - // Version 06: Convert Event store. Must always be executed first. - if (version < 6) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 07: Introduces AppId for backups. - else if (version < 7) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots. - if (version < 5) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 12: Introduce roles. - else if (version < 12) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 09: Grain indexes. - if (version < 9) - { - yield return serviceProvider.GetService(); - } - - // Version 19: Unify indexes. - if (version < 19) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 11: Introduce content drafts. - if (version < 11) - { - yield return serviceProvider.GetService(); - yield return serviceProvider.GetRequiredService(); - } - - // Version 13: Json refactoring - if (version < 13) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 14: Schema refactoring - if (version < 14) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 01: Introduce app patterns. - if (version < 1) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 15: Introduce custom full text search actors. - if (version < 15) - { - yield return serviceProvider.GetRequiredService(); - } - - // Version 17: Rename slug field. - if (version < 17) - { - yield return serviceProvider.GetService(); - } - - // Version 18: Rebuild assets. - if (version < 18) - { - yield return serviceProvider.GetService(); - } - - // Version 16: Introduce file name slugs for assets. - if (version < 16) - { - yield return serviceProvider.GetRequiredService(); - } - - yield return serviceProvider.GetRequiredService(); - } - } -} diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs deleted file mode 100644 index 4509e0956..000000000 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01.Migrations -{ - public sealed class AddPatterns : IMigration - { - private readonly InitialPatterns initialPatterns; - private readonly ICommandBus commandBus; - private readonly IAppsIndex indexForApps; - - public AddPatterns(InitialPatterns initialPatterns, ICommandBus commandBus, IAppsIndex indexForApps) - { - this.indexForApps = indexForApps; - this.initialPatterns = initialPatterns; - this.commandBus = commandBus; - } - - public async Task UpdateAsync() - { - var ids = await indexForApps.GetIdsAsync(); - - foreach (var id in ids) - { - var app = await indexForApps.GetAppAsync(id); - - if (app.Patterns.Count == 0) - { - foreach (var pattern in initialPatterns.Values) - { - var command = - new AddPattern - { - Actor = app.CreatedBy, - AppId = id, - Name = pattern.Name, - PatternId = Guid.NewGuid(), - Pattern = pattern.Pattern, - Message = pattern.Message - }; - - await commandBus.PublishAsync(command); - } - } - } - } - } -} \ No newline at end of file diff --git a/tools/Migrate_01/Migrations/ConvertEventStore.cs b/tools/Migrate_01/Migrations/ConvertEventStore.cs deleted file mode 100644 index 4a4fc7e7f..000000000 --- a/tools/Migrate_01/Migrations/ConvertEventStore.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01.Migrations -{ - public sealed class ConvertEventStore : IMigration - { - private readonly IEventStore eventStore; - - public ConvertEventStore(IEventStore eventStore) - { - this.eventStore = eventStore; - } - - public async Task UpdateAsync() - { - if (eventStore is MongoEventStore mongoEventStore) - { - var collection = mongoEventStore.RawCollection; - - var filter = Builders.Filter; - - var writesBatches = new List>(); - - async Task WriteAsync(WriteModel model, bool force) - { - if (model != null) - { - writesBatches.Add(model); - } - - if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) - { - await collection.BulkWriteAsync(writesBatches); - - writesBatches.Clear(); - } - } - - await collection.Find(new BsonDocument()).ForEachAsync(async commit => - { - foreach (BsonDocument @event in commit["Events"].AsBsonArray) - { - var meta = JObject.Parse(@event["Metadata"].AsString); - - @event.Remove("EventId"); - @event["Metadata"] = meta.ToBson(); - } - - await WriteAsync(new ReplaceOneModel(filter.Eq("_id", commit["_id"].AsString), commit), false); - }); - - await WriteAsync(null, true); - } - } - } -} diff --git a/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs b/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs deleted file mode 100644 index e1ee4b7e0..000000000 --- a/tools/Migrate_01/Migrations/ConvertEventStoreAppId.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; - -namespace Migrate_01.Migrations -{ - public sealed class ConvertEventStoreAppId : IMigration - { - private readonly IEventStore eventStore; - - public ConvertEventStoreAppId(IEventStore eventStore) - { - this.eventStore = eventStore; - } - - public async Task UpdateAsync() - { - if (eventStore is MongoEventStore mongoEventStore) - { - var collection = mongoEventStore.RawCollection; - - var filterer = Builders.Filter; - var updater = Builders.Update; - - var writesBatches = new List>(); - - async Task WriteAsync(WriteModel model, bool force) - { - if (model != null) - { - writesBatches.Add(model); - } - - if (writesBatches.Count == 1000 || (force && writesBatches.Count > 0)) - { - await collection.BulkWriteAsync(writesBatches); - - writesBatches.Clear(); - } - } - - await collection.Find(new BsonDocument()).ForEachAsync(async commit => - { - UpdateDefinition update = null; - - var index = 0; - - foreach (BsonDocument @event in commit["Events"].AsBsonArray) - { - var data = JObject.Parse(@event["Payload"].AsString); - - if (data.TryGetValue("appId", out var appIdValue)) - { - var appId = NamedId.Parse(appIdValue.ToString(), Guid.TryParse).Id.ToString(); - - var eventUpdate = updater.Set($"Events.{index}.Metadata.{SquidexHeaders.AppId}", appId); - - if (update != null) - { - update = updater.Combine(update, eventUpdate); - } - else - { - update = eventUpdate; - } - } - - index++; - } - - if (update != null) - { - var write = new UpdateOneModel(filterer.Eq("_id", commit["_id"].AsString), update); - - await WriteAsync(write, false); - } - }); - - await WriteAsync(null, true); - } - } - } -} diff --git a/tools/Migrate_01/RebuildRunner.cs b/tools/Migrate_01/RebuildRunner.cs deleted file mode 100644 index c14120d5a..000000000 --- a/tools/Migrate_01/RebuildRunner.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Migrate_01.Migrations; -using Squidex.Infrastructure; - -namespace Migrate_01 -{ - public sealed class RebuildRunner - { - private readonly Rebuilder rebuilder; - private readonly PopulateGrainIndexes populateGrainIndexes; - private readonly RebuildOptions rebuildOptions; - - public RebuildRunner(Rebuilder rebuilder, IOptions rebuildOptions, PopulateGrainIndexes populateGrainIndexes) - { - Guard.NotNull(rebuilder, nameof(rebuilder)); - Guard.NotNull(rebuildOptions, nameof(rebuildOptions)); - Guard.NotNull(populateGrainIndexes, nameof(populateGrainIndexes)); - - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - this.populateGrainIndexes = populateGrainIndexes; - } - - public async Task RunAsync(CancellationToken ct) - { - if (rebuildOptions.Apps) - { - await rebuilder.RebuildAppsAsync(ct); - } - - if (rebuildOptions.Schemas) - { - await rebuilder.RebuildSchemasAsync(ct); - } - - if (rebuildOptions.Rules) - { - await rebuilder.RebuildRulesAsync(ct); - } - - if (rebuildOptions.Assets) - { - await rebuilder.RebuildAssetsAsync(ct); - } - - if (rebuildOptions.Contents) - { - await rebuilder.RebuildContentAsync(ct); - } - - if (rebuildOptions.Indexes) - { - await populateGrainIndexes.UpdateAsync(); - } - } - } -}